#!/usr/bin/python3

"""
Split MVS 3.8j TK4- Spool files

DESCRIPTION:
	1) Takes print outputs generated by MVS 3.8j TK4-.
		a) either from an already ASCII file
		b) or by listening directly to a sockdev
	2) Splits it into one file per job.
	3) Converts each job print out to PDF.

DEPENDENCIES:
	- MVS 3.8j TK4-
	- Phyton 3
	- enscript and ps2pdf
	  need to installed and in the path of your computer running this.

BUGS:
	* Some error when processing CLASS Z printouts
		UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd7 in position 0: invalid continuation byte

CHANGELOG:
	v0.4.2: Beta release - Solved bug because of typo on the months' list
	v0.4.1: Beta release - Solved bug on conversion to 24h format.
	v0.4.0: Beta release - Separated Programmer's Name from Job Name.
						 - Reverted v0.3.0, as it doesn't solve the issue.
	v0.3.0: Beta release - Solved bug that created a blank page at the beginning.
	v0.2.0: Beta release - If jobDateYear is the same as current date, add century to the filename (e.g. 20 => 2020).
	v0.1.1: Beta release - Solved bugs with PDF filename: jobType missing, no leading zeros when hour < 10, job name truncated.
	v0.1.0: Beta release - First release.

---------------------------LICENSE NOTICE-------------------------------- 
MIT License

Copyright (c) 2020 David Asta

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import sys
import argparse
import subprocess
import os
import socket
import datetime

__pgmname__ = "mvssplitspl"
__version__ = "v0.4.2 Beta"

################################################################
class SplitSpool:
	############################################################
	# class init
	def __init__(self, outdir, seproom, sepmsgclass, debug):
		self.outputdir = outdir
		# Job information
		self.jobType = ''
		self.jobNumber = ''
		self.jobName = ''
		self.jobProgrammerName = ''
		self.jobRoom = ''
		self.jobTimeHour = ''
		self.jobTimeMinutes = ''
		self.jobTimeSeconds = ''
		self.jobTimeAMPM = ''
		self.jobDateDay = ''
		self.jobDateMonth = ''
		self.jobDateYear = ''
		self.jobPrinter = ''
		self.jobMSGCLASS = ''

		self.sockdev = ''
		self.jobLines = list()
		self.countEndJobLine = 0
		self.endJob = False
		self.months = ["", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
		
		self.seproom = seproom
		self.sepmsgclass = sepmsgclass
		self.debug = debug
		self.outputfilename = ''

	############################################################
	# Create a socket and connect to specified Address and Port
	def connectToSocket(self, hostaddr, hostport):
		# Create socket
		try:
			self.sockdev = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		except socket.error as err:
			print('Error creating socket %s', err)
			sys.exit(1)

		# Connect to socket
		try:
			self.sockdev.connect((hostaddr, int(hostport)))
			print('Connected to: ' + hostaddr + ":" + hostport)
		except socket.error as err:
			print('Error connecting to ' + hostaddr + ":" + hostport)
			print(err)
			sys.exit(1)

	############################################################
	# Listen to whatever is coming out of the MVS sockdev
	#  and for each line call processLine
	# Lines are detected by Line Feed (\n)
	def listenToSocket(self):
		data = ''
		buffer = ''
		#isFirstChar = True

		while True:
			data = self.sockdev.recv(1).decode('utf-8')

			# v0.3.0 - When a 0x0C character is found 
			#          as the first character, it makes
			#          a Form Feed (FF), which creates
			#          a blank page at the beginning.
			#if isFirstChar:
			#	isFirstChar = False
			#	if hex(ord(data[0:1])) == '0xc':
			#		data = ' '

			buffer = buffer + data
			if data == '\n': # Line Feed
				self.processLine(buffer)
				buffer = ''

	############################################################
	# Read all lines of the input file,
	#  and for each line call processLine
	def readFile(self, inputfile):
		isFirstChar = True
		self.prtfile = open(inputfile, "r")

		while True:
			line = self.prtfile.readline()

			if not line:
				break

			# v0.3.0 - When a 0x0C character is found 
			#          as the first character, it makes
			#          a Form Feed (FF), which creates
			#          a blank page at the beginning.
			#if isFirstChar:
			#	isFirstChar = False
			#	if hex(ord(line[0:1])) == '0xc':
			#		print("Initial FF removed")
			#		print(hex(ord(line[0:1])))
			#		line = ' ' + line[1:]

			self.processLine(line)

		self.prtfile.close()

	############################################################
	# For a line,
	#  extract job's information if is START/CONT
	#  call createJobFile if is END
	def processLine(self, line):
		# Line with START, identifies first page of the printout
		if (line.find("START", 7, 12) > 0) \
			  or (line.find("CONT", 7, 12) > 0):
				self.extractJobInfo(line)
		# Line with END, identifies last page of the printout
		elif line.find("END", 7, 12) > 0:
			self.countEndJobLine = self.countEndJobLine + 1
			if self.countEndJobLine == 4:
				self.endJob = True
		# Normal line
		self.jobLines.append(line)

		if self.endJob:
			# All lines of all pages of this job were read
			self.createJobFile(self.jobLines)
			self.jobLines.clear()
			self.endJob = False
			self.countEndJobLine = 0

	############################################################
	# Get Job's information details,
	#   used to compose the final filename of the PDF
	def extractJobInfo(self, line):
		self.jobMSGCLASS		= line[4:5]
		self.jobNumber			= line[18:22].strip()
		self.jobName			= line[24:32].strip()
		self.jobProgrammerName	= line[34:55].strip()
		self.jobRoom			= line[61:65].strip()
		self.jobTimeHour		= (str(line[67:69]).strip()).zfill(2)
		self.jobTimeMinutes		= line[70:72]
		self.jobTimeSeconds		= line[73:75]
		self.jobTimeAMPM		= line[76:78]
		self.jobDateDay			= line[79:81]
		self.jobDateMonth		= str(self.months.index(line[82:85])).zfill(2)
		self.jobDateYear		= line[86:88]
		self.jobPrinter			= line[90:98]

		# If necessary, convert hours to 24h format
		self.convertTo24h(int(self.jobTimeHour))

		# Detect type of job
		if line.find("JOB", 14,17) > 0:
			self.jobType = 'J'
		elif line.find("STC", 14,17) > 0:
			self.jobType = 'S'
		elif line.find("TSU", 14,17) > 0:
			self.jobType = 'T'

	############################################################
	# Create file with Job's print out in TXT format
	# Call enscript to convert from TXT to PS
	# Call ps2pdf to convert from PS to PDF
	#   The PDF is created at the outdir specified in the
	#    parameters when this script was called
	# Remove TXT and PS files after the conversion to PDF is done
	def createJobFile(self, jobLines):
		filename = self.composeOutputFilename()
		dirname = self.composeOutputDirectory()
		filename_prt = dirname + filename + ".prt"
		filename_ps = dirname + filename + ".ps"
		filename_pdf = dirname + "/" + filename + ".pdf"

		print(filename_pdf)

		outputfile = open(filename_prt, "w")

		for line in jobLines:
			outputfile.write(line)

		outputfile.close()

		# Call external programs enscript and ps2pdf to get the final PDF file
		subprocess.run(["enscript", "--quiet", "--font=Courier-Bold@8", "-l", "-H1", "-r", \
						"--margins=25:25:40:40", "-p", "- ", filename_prt, "-o", filename_ps])
		if not self.debug:
			os.remove(filename_prt)
		subprocess.run(["ps2pdf", filename_ps, filename_pdf])
		os.remove(filename_ps)

	############################################################
	# Compose the final PDF filename, 
	#   based on the job information
	def composeOutputFilename(self):
		# example: 200609_113744_A_PRINTER1-ROOM_J942_PRINTSR
		
		# v0.2.0 - If jobDateYear is the same as current date, 
		#          add century to the filename (e.g. 20 => 2020)
		yearnow = str(datetime.datetime.now().year)[2:4]
		centurynow = str(datetime.datetime.now().year)[0:2]
		if(int(yearnow) == int(self.jobDateYear)):
			self.jobDateYear = str(datetime.datetime.now().year)
		
		filename = \
			self.jobDateYear + self.jobDateMonth + self.jobDateDay \
			+ "_" + self.jobTimeHour + self.jobTimeMinutes + self.jobTimeSeconds \
			+ "_" + self.jobMSGCLASS \
			+ "_" + self.jobPrinter \

		# If there is a ROOM name (/*JOBPARM ROOM=xxxx), added it.
		if self.jobRoom:
			filename = filename \
				+ '-' + self.jobRoom

		filename = filename \
			+ "_" + self.jobType + self.jobNumber \
			+ "_" + self.jobName

		if self.jobProgrammerName:
			filename = filename \
			+ "_" + self.jobProgrammerName

		return filename

	############################################################
	# Compose the output dir,
	#   based in parameters outdir, seproom and sepmsgclass
	# If not existing, create directory
	def composeOutputDirectory(self):
		dirname = ''

		if self.seproom or self.sepmsgclass:
			if self.seproom and self.sepmsgclass:
				# Separate both by ROOM and MSGCLASS
				if self.jobRoom: # Is there actually a ROOM specified in the printout?
					self.createDir(self.outputdir + "/ROOM-" + self.jobRoom)
					self.createDir(self.outputdir + "/ROOM-" + self.jobRoom + "/CLASS" + self.jobMSGCLASS)
					dirname = self.outputdir + "/ROOM-" + self.jobRoom + "/CLASS" + self.jobMSGCLASS
				else:
					self.createDir(self.outputdir + "/CLASS" + self.jobMSGCLASS)
					dirname = self.outputdir + "/CLASS" + self.jobMSGCLASS
			elif self.seproom:
				# Separate by ROOM only
				if self.jobRoom: # Is there actually a ROOM specified in the printout?
					self.createDir(self.outputdir + "/ROOM-" + self.jobRoom)
					dirname = self.outputdir + "/ROOM-" + self.jobRoom
				else:
					self.createDir(self.outputdir)
					dirname = self.outputdir
			elif self.sepmsgclass:
				# Separate by MSGCLASS only
				self.createDir(self.outputdir + "/CLASS" + self.jobMSGCLASS)
				dirname = self.outputdir + "/CLASS" + self.jobMSGCLASS
		else:
			self.createDir(self.outputdir)

		return dirname

	############################################################
	# Try to create directory
	def createDir(self, dirname):
		if not os.path.exists(dirname):
			try:
				os.mkdir(dirname)
				print("Directory ", dirname, " created.")
			except OSError as err:
				print("Directory ", dirname, " CANNOT be created.")
				print(err)
				sys.exit(1)

	############################################################
	# Convert hour to 24h format
	#   convert 12AM to 00
	#   convert from 1PM to 11Pm, to 13 to 23
	def convertTo24h(self, hour):
		hour24 = hour

		if self.jobTimeAMPM == "PM":
			if hour < 12: # convert from 1PM to 11PM, to 13 to 23
				hour24 = hour + 12
		elif hour == 12: # convert 12AM to 00
			hour24 = 0

		self.jobTimeHour = str(hour24).zfill(2)

################################################################
if __name__ == "__main__":
	# Check that parameter have been received
	parser = argparse.ArgumentParser(	prog=__pgmname__, \
										description="Split MVS 3.8j TK4- Spool files", \
										epilog=__pgmname__ + " " + __version__)
	parser.add_argument("-v", "--version", action="version", version="%(prog)s {version}".format(version=__version__))
	parser.add_argument("-in", "--infile", help="input file (file generated by MVS)")
	parser.add_argument("-a", "--address", help="IP address of host's sockdev")
	parser.add_argument("-p", "--port", help="Port of host's sockdev")
	parser.add_argument("outdir", help="destination directory for PDFs")
	parser.add_argument("-r", "--seproom", action="store_true", default=False, help="Separate outputs per ROOM. Directory created if needed.")
	parser.add_argument("-c", "--sepmsgclass", action="store_true", default=False, help="Separate outputs per MSGCLASS. Directory created if needed.")
	parser.add_argument("-d", "--debug", action="store_true", default=False, help="Do not delete .prt files")

	args = parser.parse_args()
	
	# remove trailing / from outdir, if exists
	if args.outdir.endswith('/'):
		args.outdir = args.outdir[0:len(args.outdir) - 1]

	splitter = SplitSpool(args.outdir, args.seproom, args.sepmsgclass, args.debug)

	if args.infile:
		# Process an already existing file
		splitter.readFile(args.infile)
	elif args.address and args.port:
		# Connect and process from a scokdev
		splitter.connectToSocket(args.address, args.port)
		splitter.listenToSocket()

	sys.exit(0)