#!/usr/bin/env python3

import os
from glob import glob
import time, datetime
import logging
import argparse, shlex
import subprocess
from pathlib import Path

from timecode import Timecode
import pyseq

import OpenImageIO as oiio
import numpy as np
import yaml
from PIL import Image


DAILIES_ROOT = Path(os.environ.get("REZ_DAILIES_ROOT",  Path(os.path.dirname(os.path.realpath(__file__))).parent))
DAILIES_CONFIG_DEFAULT = DAILIES_ROOT/"config/dailies-config.yaml"
DAILIES_DATA = DAILIES_ROOT/"data"

DEFAULT_CODEC = 'avchq'
DEFAULT_DAILIES_PROFILE = 'delivery'

DEBUG = False

log = logging.getLogger(__name__)

class GenerateDaily():

	def __init__(self):
		"""
		Initial setup: gather and validate config and input data.
		Args:
			None
		Returns:
			Nothing is returned. If self.setup_success = is True, it is ready to process()
		"""

		self.start_time = time.time()
		self.setup_success = False


		# Parse Config File
		DAILIES_CONFIG = str(os.environ.get('DAILIES_CONFIG', DAILIES_CONFIG_DEFAULT))

		# Get Config file data
		if os.path.isfile(DAILIES_CONFIG):
			with open(DAILIES_CONFIG, 'r') as configfile:
				config = yaml.full_load(configfile)
		else:
			print("Error: Could not find config file %s" % DAILIES_CONFIG)
			self.setup_success = False
			return

		# Get dictionaries from config file
		self.globals_config = config.get("globals")
		output_codecs = config.get("output_codecs")
		dailies_profiles = config.get("dailies_profiles")
		ocio_profiles = config.get("ocio_profiles")


		# Parse input arguments
		parser = argparse.ArgumentParser(description='Process given image sequence with ocio display, resize and output to ffmpeg for encoding into a dailies movie.')
		parser.add_argument("inputPath", help="Input exr image sequence. Can be a folder containing images, a path to the first image, a percent 05d path, or a ##### path.")
		codecString = '"%s"' % '", "'.join(output_codecs.keys())
		parser.add_argument("-c", "--codec", help="Codec names defined in dailies-config.yaml: %s, default is \"%s\"" % (codecString, DEFAULT_CODEC))
		dailiesString = '"%s"' % '", "'.join(dailies_profiles.keys())
		parser.add_argument("-p", "--profile", help="Dailies overlay profiles defined in dailie-config.yaml: %s. Default is \"%s\"" % (dailiesString, DEFAULT_DAILIES_PROFILE))
		parser.add_argument("-o", "--output", help="Output directory: Optional override to movie_location in the DAILIES_CONFIG. This can be a path relative to the image sequence.")
		parser.add_argument("-ot", '--overlayText', help="Text elements and contents overlay on frames: e.g. \n\t\"artist= Jed Smith : comment= this is stupid man: framecounter : sequence")
		parser.add_argument("-st", '--slateText', help="Text elements and contents for slate: e.g. \n\t\"artist=Jed Smith : comment= this is stupid man")
		ocioString = '"%s"' % '", "'.join(ocio_profiles.keys())
		parser.add_argument("--ocio", help="OCIO color space presets defined in dailie-config.yaml: %s" % (ocioString))
		parser.add_argument("-d", "--debug", help="Set debug to true.", action="store_true")
		args = parser.parse_args()

		#
		inputPath = args.inputPath
		if not inputPath:
			inputPath = os.getcwd()
		
		#
		self.outputMovieDir = args.output

		#
		outputCodec = args.codec
		if not outputCodec:
			config_default_codec = self.globals_config.get('output_codec')
			if config_default_codec:
				outputCodec = config_default_codec
			else:
				outputCodec = DEFAULT_CODEC
		if outputCodec not in output_codecs:
			print("Error: invalid codec specified. Possible options are: %s" % codecString)
			self.setup_success = False
			return
		self.codec_config = config["output_codecs"][outputCodec]

		#
		outputDailiesProfile = args.profile
		if not outputDailiesProfile:
			outputDailiesProfile = DEFAULT_DAILIES_PROFILE
		self.profile_config = config.get("dailies_profiles")[outputDailiesProfile]
		self.profile_config['name'] = outputDailiesProfile

		#
		self.outputOCIOProfile = args.ocio

		#
		DEBUG = args.debug

		#
		self.overlayTextDict = {}
		for t in args.overlayText.split(':'):
			tmp=t.strip().split('=')
			if len(tmp)==2:
				self.overlayTextDict[tmp[0]]=tmp[1]
			else:
				self.overlayTextDict[tmp[0]] = True
		self.overlayTextDict['date'] = datetime.datetime.now().strftime("%Y/%m/%d")


		# Gather image sequences from input path
		self.image_sequences = self.get_image_sequences(inputPath)
		if not self.image_sequences:
			print("No image sequence found! Exiting...")
			self.setup_success = False
			return
		if 'sequence' in self.overlayTextDict.keys():
			self.overlayTextDict['sequence'] = self.image_sequences[0].path()


		# -----------------------------------
		# Set up OCIO color transform
		# -----------------------------------

		# Get OCIO config file from dailies-config.yaml or from ENV
		self.ocioconfig = self.globals_config.get('ocioconfig')
		if not self.ocioconfig:
			env_ocio = os.getenv("OCIO")
			if env_ocio:
				self.ocioconfig = env_ocio

		if not os.path.exists(self.ocioconfig):
			log.warning("OCIO Config does not exist: \n\t{0}\n\tNo OCIO color transform will be applied".format(
				self.ocioconfig))
			self.ocioconfig = None

		# Get default ocio transform to use if none is passed by commandline
		ocio_default_transform = self.globals_config.get("ocio_default_transform")

		# Set self.outputOCIOTransform: the colorspace transformation to use in processing
		if self.outputOCIOProfile:
			# Check if specified ocio profile exists in the config
			if self.outputOCIOProfile in ocio_profiles.keys():
				self.outputOCIOTransform = ocio_profiles.get(self.outputOCIOProfile).get('ociocolorconvert')
			else:
				print("Error: OCIO color transform %s does not exist in config. Falling back to default %s" % (self.outputOCIOProfile, ocio_default_transform))
				self.outputOCIOTransform = ocio_profiles.get(ocio_default_transform).get('ociocolorconvert')
				self.outputOCIOProfile = ocio_default_transform
		elif ocio_default_transform:
			self.outputOCIOProfile = ocio_default_transform
			self.outputOCIOTransform = ocio_profiles.get(ocio_default_transform).get('ociocolorconvert')
		else:
			# No ocio color transform specified
			print("Warning: No default ocio transform specified, and no transform specified on the commandline. No color transform will occur.")
			self.outputOCIOTransform = None

		# Only colorconvert is implemented for now
		# self.ociolook = self.globals_config.get('ociolook')
		# self.ociodisplay = self.globals_config.get('ociodisplay')
		# self.ocioview = self.globals_config.get('ocioview')



		# Anything with the same name in the codec config overrides the globals
		for key, value in self.codec_config.items():
			if key in self.globals_config:
				if self.codec_config[key]:
					self.globals_config[key] = value

		# Get output width and height
		self.output_width = self.globals_config['width']
		self.output_height = self.globals_config['height']

		# If output width or height is not defined, we need to calculate it from the input images
		if not self.output_width or not self.output_height:
			buf = oiio.ImageBuf(self.image_sequence[0].path)
			spec = buf.spec()
			print(spec)
			iar = float(spec.width) / float(spec.height)
			if not self.output_width:
				self.output_width = spec.width
				self.globals_config['width'] = self.output_width
			if not self.output_height:
				self.output_height = int(round(self.output_width / iar))
				self.globals_config['height'] = self.output_height
			# buf.close()

		self.setup_success = True


		if self.setup_success == True:
			for self.image_sequence in self.image_sequences:
				self.process()


	def process(self):
		"""
		Performs the actual processing of the movie.
		Args:
			None
		Returns:
			None
		"""


		# Set up movie file location and naming

		# Crop separating character from sequence basename if there is one.
		seq_basename = self.image_sequence.head()

		if seq_basename.endswith(self.image_sequence.parts[-2]):
			seq_basename = seq_basename[:-1]

		movie_ext = self.globals_config['movie_ext']

		# Create full movie filename
		# Append codec to dailies movie name if requested
		if self.globals_config['movie_append_codec']:
			codec_name = self.codec_config.get('name')
			if not codec_name:
				print("No codec name! Please fix the config!")
				print(self.codec_config)
				codec_name = ""
			else:
				movie_basename = seq_basename + "_" + codec_name
				movie_filename = movie_basename + "." + movie_ext
		else:
			movie_basename = seq_basename
			movie_filename = seq_basename + "." + movie_ext


		# Handle relative / absolute paths for movie location
		# use globals config for movie location if none specified on the commandline
		if not self.outputMovieDir:
			self.outputMovieDir = self.globals_config['movie_location']

		if self.outputMovieDir.startswith('/'):
			# Absolute path specified
			self.outputMovieFile = os.path.join(self.outputMovieDir, movie_filename)
		elif self.outputMovieDir.startswith("~"):
			# Path referencing home folder specified
			self.outputMovieDir = os.path.expanduser(self.outputMovieDir)
			self.outputMovieFile = os.path.join(self.outputMovieDir, movie_filename)
		elif self.outputMovieDir.startswith(".") or self.outputMovieDir.startswith(".."):
			# Relative path specified - will output relative to image sequence directory
			self.outputMovieFile = os.path.join(self.image_sequence.dirname, self.outputMovieDir, movie_filename)
		else:
			self.outputMovieFile = os.path.join(self.outputMovieDir, movie_filename)

		# Check output dir exists
		if not os.path.exists(os.path.dirname(self.outputMovieFile)):
			try:
				os.makedirs(os.path.dirname(self.outputMovieFile))
			except OSError:
				print("Output directory does not exist and do not have permission to create it: \n\t{0}".format(os.path.dirname(self.outputMovieFile)))
				return
		
		# Echo settings
		print("Input sequence: \t%s" % self.image_sequence.path())
		print("Dailies profile: \t%s" % self.profile_config['name'])
		print("Codec config: \t\t%s" % self.codec_config['name'])
		print("OCIO profile: \t\t%s" % self.outputOCIOProfile)
		print("OCIO transform: \t%s" % self.outputOCIOTransform)
		print("Output width: \t\t%s" % self.output_width)
		print("Output height: \t\t%s" % self.output_height)
		print("Output movie: \t\t%s" % self.outputMovieFile)
		print("Text:")
		for k,v in self.overlayTextDict.items():
			print("\t%s: %s" % (k,v))
		print("")


		# Set up Logger
		log_fullpath = os.path.splitext(self.outputMovieFile)[0] + ".log"
		if os.path.exists(log_fullpath):
			os.remove(log_fullpath)
		handler = logging.FileHandler(log_fullpath)
		handler.setFormatter(
			logging.Formatter('%(levelname)s\t %(asctime)s \t%(message)s', '%Y-%m-%dT%H:%M:%S')
			)
		log.addHandler(handler)
		if self.globals_config['debug']:
			log.setLevel(logging.DEBUG)
		else:
			log.setLevel(logging.INFO)
		log.debug("Input sequence: %s" % self.image_sequence.path())
		log.debug("Dailies profile: %s" % self.profile_config['name'])
		log.debug("Codec config: %s" % self.codec_config['name'])
		log.debug("OCIO transform: %s" % self.outputOCIOTransform)
		log.debug("Output width: %s" % self.output_width)
		log.debug("Output height: %s" % self.output_height)
		log.debug("Output movie: %s" % self.outputMovieFile)

		# Set pixel_data_type based on config bitdepth
		if self.codec_config['bitdepth'] > 8:
			self.pixel_data_type = oiio.UINT16
		else:
			self.pixel_data_type = oiio.UINT8


		# Get timecode based on frame
		tc = Timecode(self.globals_config['framerate'], start_timecode='00:01:00:00')
		self.start_tc = tc + self.image_sequence.start()

		# Set up ffmpeg command
		ffmpeg_args = self.setup_ffmpeg()

		log.info("\n\n%s\n\n" % (ffmpeg_args))
		
		print("Running command:\n%s\n"% ffmpeg_args)

		# Static image buffer for text that doesn't change frame to frame
		self.static_text_buf = oiio.ImageBuf(oiio.ImageSpec(self.output_width, self.output_height, 4, self.pixel_data_type))


		# Loop through each text element, create the text image, and add it to self.static_text_buf
		log.debug("\t-- processing static text")
		text_elements = self.profile_config.get('text_elements')
		if text_elements:
			for text_element_name, text_element in text_elements.items():
				if text_element_name != 'framecounter':
					self.generate_text(text_element_name, text_element, self.static_text_buf)


		if not DEBUG:
			# Invoke ffmpeg subprocess
			ffproc = subprocess.Popen(
				shlex.split(ffmpeg_args),
				stdin=subprocess.PIPE,
				stdout=subprocess.PIPE
				)

		self.slate = oiio.ImageBuf(oiio.ImageSpec(self.output_width, self.output_height, 3, self.pixel_data_type))
		oiio.ImageBufAlgo.fill(self.slate, (1,0,0), (0,0,0), oiio.ROI.All)
		pixels = self.slate.get_pixels(self.pixel_data_type)
		ffproc.stdin.write(pixels)


		# Loop through every frame, passing the result to the ffmpeg subprocess
		for i, self.frame in enumerate(self.image_sequence, 1):
			log.info("\n\nProcessing %s, %s of %s" % (self.frame, i, self.image_sequence.length()))
			# elapsed_time = datetime.timedelta(seconds = time.time() - start_time)
			# log.info("Time Elapsed: \t{0}".format(elapsed_time))
			frame_start_time = time.time()

			buf = self.process_frame(self.frame)

			# Set framecounter in text elements, add framecounter text
			if self.overlayTextDict:
				if self.overlayTextDict.get('framecounter'):
					self.overlayTextDict['framecounter'] = ("%s %s" % ("", str(self.frame.frame).zfill(text_elements.get('framecounter').get('padding'))))
					log.debug("\t-- processing dynamic text")
					buf = self.generate_text('framecounter', text_elements.get('framecounter'), buf)


			if not DEBUG:
				# If MJPEG: convert from raw byte data to jpeg before passing to ffmpeg for concatenation
				pixels = buf.get_pixels(self.pixel_data_type)
				if self.codec_config['name'] == 'mjpeg':
					jpeg_img = Image.fromarray(pixels)
					# https://pillow.readthedocs.io/en/5.2.x/handbook/image-file-formats.html#jpeg
					jpeg_img.save(ffproc.stdin, "JPEG", subsampling="4:4:4", quality=90)
				else:
					ffproc.stdin.write(pixels)
			else:
				buf.write(os.path.splitext(self.outputMovieFile)[0] + ".{0:05d}.jpg".format(self.frame.frame))

			frame_elapsed_time = datetime.timedelta(seconds=time.time() - frame_start_time)
			log.info("Frame Processing Time: \t{0}".format(frame_elapsed_time))

		if not DEBUG:
			result, error = ffproc.communicate()
		elapsed_time = datetime.timedelta(seconds = time.time() - self.start_time)
		log.info("Total Processing Time: \t{0}".format(elapsed_time))




	def get_image_sequences(self, input_path):
		"""
		Get list of image sequence objects given a path on disk.

		Args:
			input_path: Input file path. Can be a directory or file or %05d / ### style

		Returns:
			An image sequence object.
		"""
		input_path = os.path.realpath(input_path)
		input_image_formats = self.globals_config.get('input_image_formats')
		print('Input path:')
		if os.path.isdir(input_path):
			# Find image sequences recursively inside specified directory
			image_sequences = []
			for root, directories, filenames in os.walk(input_path):
				# If there is more than 1 image file in input_path, search this path for file sequences also
				if root == input_path:
					image_files = [f for f in filenames if os.path.splitext(f)[-1][1:] in input_image_formats]
					if len(image_files) > 1:
						image_sequences += pyseq.get_sequences(input_path)
				for directory in directories:
					image_sequences += pyseq.get_sequences(os.path.join(root, directory))
			if not image_sequences:
				log.error("Could not find any image files recursively in source directory: {0}".format(input_path))
				return None
		elif os.path.isfile(input_path):
			# Assume it's the first frame of the image sequence
			# Try to split off the frame number to get a glob
			image = pyseq.get_sequences(input_path)
			if image:
				image = image[0]
			image_sequences = pyseq.get_sequences(os.path.join(image.dirname, image.name.split(image.parts[-2])[0]) + "*")

		else:
			# Assume this is a %05d or ### image sequence. Use the parent directory if it exists.
			dirname, filename = os.path.split(input_path)
			if os.path.isdir(dirname):
				image_sequences = pyseq.get_sequences(dirname)
			else:
				image_sequences = None

		if image_sequences:
			# Remove image sequences not in list of approved extensions
			if not input_image_formats:
				input_image_formats = ['exr']
			actual_image_sequences = []
			for image_sequence in image_sequences:
				extension = image_sequence.name.split('.')[-1]
				if extension in input_image_formats:
					actual_image_sequences.append(image_sequence)
			print("Found image sequence%s:" % ('s' if len(actual_image_sequences) > 1 else ""))
			for seq in actual_image_sequences:
				print("\t%s/%s" % (input_path, seq.format('%h%R%t')))
			print("")
			return actual_image_sequences
		else:
			log.error("Could not find any Image Sequences!!!")
			return None



	def setup_ffmpeg(self):
		"""
		Constructs an ffmpeg command based on the given codec config.

		Returns:
			A string containing the entire ffmpeg command to run.
		"""

		# ffmpeg-10bit No longer necessary in ffmpeg > 4.1
		ffmpeg_command = "ffmpeg"

		if self.codec_config['bitdepth'] >= 10:
			pixel_format = "rgb48le"
		else:
			pixel_format = "rgb24"

		if self.codec_config['name'] == 'mjpeg':
			# Set up input arguments for frame input through pipe:
			args = "{0} -y -framerate {1} -i pipe:0".format(ffmpeg_command, self.globals_config['framerate'])
		else:
			# Set up input arguments for raw video and pipe:
			args = "{0} -hide_banner -loglevel info -y -f rawvideo -pixel_format {1} -video_size {2}x{3} -framerate {4} -i pipe:0".format(
				ffmpeg_command, pixel_format, self.globals_config['width'], self.globals_config['height'], self.globals_config['framerate'])

		# Add timecode so that start frame will display correctly in RV etc
		args += " -timecode {0}".format(self.start_tc)

		if self.codec_config['codec']:
			args += " -c:v {0}".format(self.codec_config['codec'])

		if self.codec_config['profile']:
			args += " -profile:v {0}".format(self.codec_config['profile'])

		if self.codec_config['qscale']:
			args += " -qscale:v {0}".format(self.codec_config['qscale'])

		if self.codec_config['preset']:
			args += " -preset {0}".format(self.codec_config['preset'])

		if self.codec_config['keyint']:
			args += " -g {0}".format(self.codec_config['keyint'])

		if self.codec_config['bframes']:
			args += " -bf {0}".format(self.codec_config['bframes'])

		if self.codec_config['tune']:
			args += " -tune {0}".format(self.codec_config['tune'])

		if self.codec_config['crf']:
			args += " -crf {0}".format(self.codec_config['crf'])

		if self.codec_config['pix_fmt']:
			args += " -pix_fmt {0}".format(self.codec_config['pix_fmt'])

		if self.globals_config['framerate']:
			args += " -r {0}".format(self.globals_config['framerate'])

		if self.codec_config['vf']:
			args += " -vf {0}".format(self.codec_config['vf'])

		if self.codec_config['vendor']:
			args += " -vendor {0}".format(self.codec_config['vendor'])

		if self.codec_config['metadata_s']:
			args += " -metadata:s {0}".format(self.codec_config['metadata_s'])

		if self.codec_config['bitrate']:
			args += " -b:v {0}".format(self.codec_config['bitrate'])

		# Finally add the output movie file path
		args += " {0}".format(self.outputMovieFile)

		return args





	def process_frame(self, frame):
		"""
		Apply all color and reformat / resize operations to input image, then return the imagebuf

		Args:
			frame: pyseq Item object describing the current frame.
			framenumber: the current frame number

		Returns:
			Returns an oiio.ImageBuf object which holds the altered image data.
		"""

		# Setup image buffer
		buf = oiio.ImageBuf(frame.path)
		spec = buf.spec()

		# Get Codec Config and gather information
		iwidth = spec.width
		iheight = spec.height
		if float(iheight) != 0:
			iar = float(iwidth) / float(iheight)
		else:
			log.error("Input height is Zero! Skipping frame {0}".format(frame))
			return

		px_filter = self.globals_config.get('filter')
		self.output_width = self.globals_config.get('width')
		self.output_height = self.globals_config.get('height')
		fit = self.globals_config.get('fit')
		cropwidth = self.globals_config.get('cropwidth')
		cropheight = self.globals_config.get('cropheight')

		# Remove alpha channel
		oiio.ImageBufAlgo.channels(buf, buf, (0,1,2))

		# Apply ocio color transform
		buf = self.apply_ocio_transform(buf)

		# Setup for width and height
		if not self.output_width:
			resize = False
		else:
			resize = True
			# If no output height specified, resize keeping aspect ratio, long side = width - calc height
			oheight_noar = int(self.output_width / iar)
			if not self.output_height:
				self.output_height = oheight_noar
			oar = float(self.output_width) / float(self.output_height)


		# Apply cropwidth / cropheight to remove pixels on edges before applying resize
		if cropwidth or cropheight:
			# Handle percentages
			if type(cropwidth) == str:
				if "%" in cropwidth:
					cropwidth = int(float(cropwidth.split('%')[0])/100*iwidth)
					log.info("Got crop width percentage: {0}px".format(cropwidth))
			if type(cropheight) == str:
				if "%" in cropheight:
					cropheight = int(float(cropheight.split('%')[0])/100*iheight)
					log.info("Got crop height percentage: {0}px".format(cropheight))

			log.debug("Not Yet CROPPED:{0} {1}".format(buf.spec().width, buf.spec().height))

			buf = oiio.ImageBufAlgo.crop(buf, roi=oiio.ROI(int(cropwidth / 2), int(iwidth - cropwidth / 2), int(cropheight / 2), int(iheight - cropheight / 2)))

			# Remove data window of buffer so resize works from cropped region
			buf.set_full(buf.roi.xbegin, buf.roi.xend, buf.roi.ybegin, buf.roi.yend, buf.roi.chbegin, buf.roi.chend)

			log.debug("CROPPED:{0} {1}".format(buf.spec().width, buf.spec().height))

			# Recalculate input resolution and aspect ratio - since it may have changed with crop
			iwidth = buf.spec().width
			iheight = buf.spec().height
			iar = float(iwidth) / float(iheight)
			oheight_noar = int(self.output_width / iar)

			log.debug("iwidth:{0} x iheight:{1} x iar: {2}".format(iwidth, iheight, iar))



		# Apply Resize / Fit
		# If input and output resolution are the same, do nothing
		# If output width is bigger or smaller than input width, first resize without changing input aspect ratio
		# If "fit" is true,
		# If output height is different than input height: transform by the output height - input height / 2 to center,
		# then crop to change the roi to the output res (crop moves upper left corner)

		identical = self.output_width == iwidth and self.output_height == iheight
		resize = not identical and resize


		if resize:
			log.debug("\t-- resizing \n\tfrom:\t %sx%s %02f\n\tto:\t\t %sx%s %02f" %(iwidth, iheight, iar, self.output_width, self.output_height, oar))

			if iwidth != self.output_width:
				# Perform resize, no change in AR
				log.debug("input width does not equal output_width: oheight noar: {0}, pxfilter: {1}".format(oheight_noar, px_filter))

				#############
				#
				if px_filter:
					# (bug): using "lanczos3", 6.0, and upscaling causes artifacts
					# (bug): dst buf must be assigned or ImageBufAlgo.resize doesn't work
					buf = oiio.ImageBufAlgo.resize(buf, px_filter, roi=oiio.ROI(0, self.output_width, 0, oheight_noar))
				else:
					buf = oiio.ImageBufAlgo.resize(buf, roi=oiio.ROI(0, self.output_width, 0, oheight_noar))

			if fit:
				# If fitting is enabled..
				height_diff = self.output_height - oheight_noar
				log.debug("Height difference: {0} {1} {2}".format(height_diff, self.output_height, oheight_noar))

				# If we are cropping to a smaller height we need to transform first then crop
				# If we pad to a taller height, we need to crop first, then transform.
				if self.output_height < oheight_noar:
					# If we are cropping...
					buf = self.oiio_transform(buf, 0, height_diff/2)
					buf = oiio.ImageBufAlgo.crop(buf, roi=oiio.ROI(0, self.output_width, 0, self.output_height))
				elif self.output_height > oheight_noar:
					# If we are padding...
					buf = oiio.ImageBufAlgo.crop(buf, roi=oiio.ROI(0, self.output_width, 0, self.output_height))
					buf = self.oiio_transform(buf, 0, height_diff/2)

		# Apply Cropmask if enabled
		cropmask_config = self.profile_config.get('cropmask')
		if cropmask_config:
			enable_cropmask = cropmask_config.get('enable')
		else:
			enable_cropmask = False
		if enable_cropmask:
			cropmask_ar = cropmask_config.get('aspect')
			cropmask_opacity = cropmask_config.get('opacity')

			if not cropmask_ar or not cropmask_opacity:
				log.error("Cropmask enabled, but no crop specified. Skipping cropmask...")
			else:
				cropmask_height = int(round(self.output_width / cropmask_ar))
				cropmask_bar = int((self.output_height - cropmask_height)/2)
				log.debug("Cropmask height: \t{0} = {1} / {2} = {3} left".format(cropmask_height, self.output_height, cropmask_ar, cropmask_bar))

				cropmask_buf = oiio.ImageBuf(oiio.ImageSpec(self.output_width, self.output_height, 4, self.pixel_data_type))

				# Fill with black, alpha = cropmask opacity
				oiio.ImageBufAlgo.fill(cropmask_buf, (0, 0, 0, cropmask_opacity))

				# Fill center with black
				oiio.ImageBufAlgo.fill(cropmask_buf, (0, 0, 0, 0), oiio.ROI(0, self.output_width, cropmask_bar, self.output_height - cropmask_bar))

				# Merge cropmask and text over image
				oiio.ImageBufAlgo.channels(buf, buf, (0, 1, 2, 1.0))
				buf = oiio.ImageBufAlgo.over(cropmask_buf, buf)
				buf = oiio.ImageBufAlgo.over(self.static_text_buf, buf)
				oiio.ImageBufAlgo.channels(buf, buf, (0,1,2))

		return buf


	def oiio_transform(self, buf, xoffset, yoffset):
		"""
		Convenience function to reposition an image.

		Args:
			buf: oiio.ImageBuf object representing the image to be transformed.
			xoffset: X offset in pixels
			yoffset: Y offset in pixels

		Returns:
			Returns the modified oiio.ImageBuf object which holds the altered image data.
		"""
		orig_roi = buf.roi
		buf.specmod().x += int(xoffset)
		buf.specmod().y += int(yoffset)
		buf_trans = oiio.ImageBuf()
		oiio.ImageBufAlgo.crop(buf_trans, buf, orig_roi)
		return buf_trans



	def apply_ocio_transform(self, buf):
		"""
		Applies an ocio transform specified in the config. Can be a ociodisplay, colorconvert, or look transform
		For now only colorconvert is supported.
		Reads from self.ocioconfig to specify the ocio config to use.
		Reads from self.outputOCIOTransform, a two item list. [0] is src, [1] is dst colorspace.

		Args:
			buf: oiio.ImageBuf object representing the image to be transformed.

		Returns:
			Returns the modified oiio.ImageBuf object which holds the altered image data.
		"""

		if self.outputOCIOTransform:
			log.debug("\t-- applying OCIO Config: %s \n\t%s  -> %s" % (self.outputOCIOProfile, self.outputOCIOTransform[0], self.outputOCIOTransform[1]))
			success = oiio.ImageBufAlgo.colorconvert(buf, buf, self.outputOCIOTransform[0], self.outputOCIOTransform[1], colorconfig=self.ocioconfig)
			if not success:
				log.error("Error: OCIO Color Convert failed. Please check that you have the specified colorspaces in your OCIO config.")

		# Only colorconvert is implemented for now.

		# if self.ociolook:
		#	 oiio.ImageBufAlgo.ociolook(buf, buf, self.ociolook, self.ocioview, colorconfig=self.ocioconfig)
		# if self.ociodisplay and self.ocioview:
		#	 # Apply OCIO display transform onto specified image buffer
		#	 success = oiio.ImageBufAlgo.ociodisplay(buf, buf, self.ociodisplay, self.ocioview, colorconfig=self.ocioconfig)

		return buf



	def generate_text(self, text_element_name, text_element, buf):
		"""
		Generate text and write it into an image buffer.

		Args:
			text_element_name: the name of the text element to search for in the config
			text_element: the config dict to use
			buf: the oiio.ImageBuf object to write the pixels into

		Returns:
			Returns the modified oiio.ImageBuf object with text added.
		"""

		font = str(DAILIES_DATA/"fonts/Helvetica/Helvetica.ttf")
		font_size = .02
		font_color =  [0.8, 0.8, 0.8, 1.0]
		box = [0.4328125, 0.9481481481481482, 0.5796875, 0.9998842592592593]

		# Text Elements
		log.debug("\t\t-- text element: %s" % (text_element_name))

		# Inherit globals if an element in text_element is not defined
		for key, value in text_element.items():
			if key in self.profile_config:
				if not text_element[key]:
					# text element key is blank, inherit global value
					text_element[key] = self.profile_config[key]
		font = text_element['font']
		if not os.path.isfile(font):
			log.error("Specified font does not exist!")
			return buf


		# Calculate font size and position
		font_size = text_element['font_size']
		font_color = text_element['font_color']
		anchor = text_element['anchor']
		hjustify = text_element['hjustify']
		vjustify = text_element['vjustify']


		# Scale back to pixels from %
		font_size = int(font_size * self.output_width)

		# Get text to display
		text_contents = self.overlayTextDict.get(text_element_name)
		text_prefix = text_element['prefix']
		if text_prefix:
			text_contents = text_prefix + text_contents

		leading = self.profile_config.get('leading')

		if text_contents:
			log.debug("\nContent: %s\nAnchor: %s, %s\nFont name: %s\nFont size: %s" % (text_contents, anchor[0], anchor[1], font, font_size))

			lines = [text_contents]
			lines.reverse()

			for line in lines:
				oiio.ImageBufAlgo.render_text(
					buf, int(anchor[0]*self.output_width), int(anchor[1]*self.output_height), line, fontsize=font_size, fontname=font,
					textcolor=(font_color[0], font_color[1], font_color[2], font_color[3]),
					alignx=hjustify, aligny=vjustify, shadow=0,
					roi=oiio.ROI.All, nthreads=0
					)
		else:
			log.warning("Warning: No text specified for text element {0}".format(text_element_name))
		return buf


if __name__=="__main__":
	daily = GenerateDaily()