#!/usr/bin/env python
# Copyright 2018-2020 Aaron Barany
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import print_function
import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile

import flatbuffers
from DeepSeaVectorDraw.FaceGroup import *
from DeepSeaVectorDraw.FileOrData import *
from DeepSeaVectorDraw.FileReference import *
from DeepSeaVectorDraw.Font import *
from DeepSeaVectorDraw.FontCacheSize import *
from DeepSeaVectorDraw.FontQuality import *
from DeepSeaVectorDraw.RawData import *
from DeepSeaVectorDraw.Resource import *
from DeepSeaVectorDraw.VectorResources import *

class VectorResources:
	"""Class containing the information for vector resources to be saved in FlatBuffer format."""
	def __init__(self, cuttlefishTool = 'cuttlefish'):
		"""
		Constructs this in the default state.

		cuttlefish is used to convert images into textures. By default it will check for
		"cuttlefish" on PATH, but the path may be passed into the constructor.
		"""

		self.cuttlefish = cuttlefishTool

	def load(self, contents, basePath = None):
		"""
		Loads the information for the VectorResources from a map.

		basePath: The base path to load relative paths from, typically relative to the original
		file.

		json format:
		{
			"textures":
			[
				{
					"name": "<name used to reference the texture>" (optional, defaults to path
						filename without extension),
					"path": "<path to image>",
					"format": "<texture format; see cuttlefish help for details>",
					"type": "<texture channel type; see cuttlefish help for details>",
					"srgb": <true|false> (optional, defaults to false),
					"size": [<width>, <height>] (optional),
					"quality": "<lowest|low|normal|high|highest>" (optional),
					"container": "<pvr|dds|ktx>" (optional, defaults to pvr),
					"embed": <true|false to embed in resource file> (optional, defaults to false)
				},
				...
			],
			"faceGroups":
			[
				{
					"name": "<name used to reference the face group>",
					"faces":
					[
						{
							"name": "<name used to reference the font>" (optional),
							"path": "<path to font file>"
						},
						...
					]
				},
				...
			],
			"fonts":
			[
				{
					"name": "<name used to reference the font>",
					"faceGroup": "<faceGroup name">,
					"faces":
					[
						"<font in faceGroup>",
						...
					],
					"quality": "<low|medium|high|veryhigh>",
					"cacheSize": "<small|large>" (optional, defaults to large),
					"embed": <true|false to embed in resource file> (optional, defaults to false)
				}
			]
		}
		"""

		self.basePath = basePath
		if 'textures' in contents:
			self.textures = contents['textures']
		if 'faceGroups' in contents:
			self.faceGroups = contents['faceGroups']
		if 'fonts' in contents:
			self.fonts = contents['fonts']

		faceGroupFaces = {}
		for faceGroup in self.faceGroups:
			name = faceGroup['name']
			faceGroupFaces[name] = {}
			for face in faceGroup['faces']:
				faceGroupFaces[name][face['name']] = face['path']
		for font in self.fonts:
			if font['faceGroup'] not in faceGroupFaces:
				raise Exception('Face group "' + font['faceGroup'] + '" not present.')
			faces = faceGroupFaces[font['faceGroup']]
			for face in font['faces']:
				if face not in faces:
					raise Exception(
						'Face "' + face + '" not in face group "' + font['faceGroup'] + '"')

	def loadJson(self, json, basePath = None):
		"""Loads from a string containing json data. See load() for expected json format."""
		self.load(json.loads(json), basePath)

	def loadStream(self, stream, basePath = None):
		"""Loads from a stream containing json data. See load() for expected json format."""
		self.load(json.load(stream), basePath)

	def loadFile(self, jsonFile):
		"""Loads from a json file. See load() for expected json format."""
		with open(jsonFile) as f:
			self.load(json.load(f), os.path.dirname(jsonFile))

	def save(self, outputPath, quiet = False, multithread = True):
		"""
		Saves the vector resources.

		This will create a directory named "<filename w/o extension>_resources" in order to hold
		the textures and font files. When moving the resources around, the file and directory
		should stay together.
		"""

		(root, filename) = os.path.split(outputPath)
		resourceDirName = os.path.splitext(filename)[0] + '_resources'
		resourceDir = os.path.join(root, resourceDirName)

		def createResourceDir():
			if not os.path.exists(resourceDir):
				os.makedirs(resourceDir)

		def createResourceData(filePath, outputName, removeEmbedded = False):
			if outputName:
				pathOffset = builder.CreateString(outputName.replace('\\', '/'))
				FileReferenceStart(builder)
				FileReferenceAddPath(builder, pathOffset)
				return FileOrData.FileReference, FileReferenceEnd(builder)
			else:
				with open(filePath, 'r+b') as dataFile:
					dataOffset = builder.CreateByteVector(dataFile.read())
				if removeEmbedded:
					os.remove(filePath)
				RawDataStart(builder)
				RawDataAddData(builder, dataOffset)
				return FileOrData.RawData, RawDataEnd(builder)

		builder = flatbuffers.Builder(0)

		textureOffsets = []
		with tempfile.NamedTemporaryFile() as tempFile:
			for texture in self.textures:
				path = texture['path']
				if 'name' in texture:
					name = texture['name']
				else:
					name = os.path.splitext(os.path.basename(path))[0]
				if 'container' in texture:
					extension = '.' + texture['container']
				else:
					extension = '.pvr'
				embed = 'embed' in texture and texture['embed']
				if embed:
					textureOutputPath = tempFile.name + extension
					outputName = None
				else:
					createResourceDir()
					outputName = os.path.join(resourceDirName, name + extension)
					textureOutputPath = os.path.join(root, outputName)

				commandLine = [self.cuttlefish, '-i', os.path.join(self.basePath, path),
					'-o', textureOutputPath, '-f', texture['format'], '-t', texture['type']]
				if quiet:
					commandLine.append('-q')
				if multithread:
					commandLine.append('-j')
				if 'srgb' in texture and texture['srgb']:
					commandLine.append('--srgb')
				if 'size' in texture:
					size = texture['size']
					commandLine.extend(['-r', size[0], size[1]])
				if 'quality' in texture:
					commandLine.extend(['-Q', texture['quality'].lower()])

				if not quiet:
					print('Converting texture "' + path + '"...')
				sys.stdout.flush()

				try:
					subprocess.check_call(commandLine)
					nameOffset = builder.CreateString(name)
					dataType, dataOffset = createResourceData(textureOutputPath, outputName, True)
				except:
					if os.path.isfile(textureOutputPath):
						os.remove(textureOutputPath)
					raise

				ResourceStart(builder)
				ResourceAddName(builder, nameOffset)
				ResourceAddDataType(builder, dataType)
				ResourceAddData(builder, dataOffset)
				textureOffsets.append(ResourceEnd(builder))

		VectorResourcesStartTexturesVector(builder, len(textureOffsets))
		for offset in reversed(textureOffsets):
			builder.PrependUOffsetTRelative(offset)
		texturesOffset = builder.EndVector(len(textureOffsets))

		faceGroupOffsets = []
		for faceGroup in self.faceGroups:
			nameOffset = builder.CreateString(faceGroup['name'])

			faces = faceGroup['faces']
			faceOffsets = []
			for face in faces:
				name = face['name']
				path = face['path']
				embed = 'embed' in face and face['embed']
				if embed:
					outputName = None
					fontOutputPath = os.path.join(self.basePath, path)
				else:
					outputName = os.path.join(resourceDirName,
						name + os.path.splitext(extension)[1])
					fontOutputPath = os.path.join(root, outputName)
					createResourceDir()
					shutil.copyfile(os.path.join(self.basePath, path), fontOutputPath)

				faceNameOffset = builder.CreateString(name)
				dataType, dataOffset = createResourceData(fontOutputPath, outputName)

				ResourceStart(builder)
				ResourceAddName(builder, faceNameOffset)
				ResourceAddDataType(builder, dataType)
				ResourceAddData(builder, dataOffset)
				faceOffsets.append(ResourceEnd(builder))

			FaceGroupStartFacesVector(builder, len(faceOffsets))
			for offset in reversed(faceOffsets):
				builder.PrependUOffsetTRelative(offset)
			facesOffset = builder.EndVector(len(faceOffsets))

			FaceGroupStart(builder)
			FaceGroupAddName(builder, nameOffset)
			FaceGroupAddFaces(builder, facesOffset)
			faceGroupOffsets.append(FaceGroupEnd(builder))

		VectorResourcesStartFaceGroupsVector(builder, len(faceGroupOffsets))
		for offset in reversed(faceGroupOffsets):
			builder.PrependUOffsetTRelative(offset)
		faceGroupsOffset = builder.EndVector(len(faceGroupOffsets))

		fontOffsets = []
		qualityValues = {'low': FontQuality.Low, 'medium': FontQuality.Medium, \
			'high': FontQuality.High, 'veryhigh': FontQuality.VeryHigh}
		cacheValues = {'small': FontCacheSize.Small, 'large': FontCacheSize.Large}
		for font in self.fonts:
			nameOffset = builder.CreateString(font['name'])
			faceGroupOffset = builder.CreateString(font['faceGroup'])

			faces = font['faces']
			faceOffsets = []
			for face in faces:
				faceOffsets.append(builder.CreateString(face))

			FontStartFacesVector(builder, len(faceOffsets))
			for offset in reversed(faceOffsets):
				builder.PrependUOffsetTRelative(offset)
			facesOffset = builder.EndVector(len(faceOffsets))

			FontStart(builder)
			FontAddName(builder, nameOffset)
			FontAddFaceGroup(builder, faceGroupOffset)
			FontAddFaces(builder, facesOffset)
			FontAddQuality(builder, qualityValues[font['quality'].lower()])
			if 'cacheSize' in font:
				cacheSize = cacheValues[font['cacheSize'].lower()]
			else:
				cacheSize = FontCacheSize.Large
			FontAddCacheSize(builder, cacheSize)
			fontOffsets.append(FontEnd(builder))

		VectorResourcesStartFontsVector(builder, len(fontOffsets))
		for offset in reversed(fontOffsets):
			builder.PrependUOffsetTRelative(offset)
		fontsOffset = builder.EndVector(len(fontOffsets))

		VectorResourcesStart(builder)
		VectorResourcesAddTextures(builder, texturesOffset)
		VectorResourcesAddFaceGroups(builder, faceGroupsOffset)
		VectorResourcesAddFonts(builder, fontsOffset)
		builder.Finish(VectorResourcesEnd(builder))

		with open(outputPath, 'wb') as f:
			f.write(builder.Output())

if __name__ == '__main__':
	parser = argparse.ArgumentParser(description =
		'Create vector resources to be used by Deep Sea.')
	parser.add_argument('-i', '--input', required = True,
		help = 'input json description of the resources')
	parser.add_argument('-o', '--output', required = True,
		help = 'output file name, typically with the extension ".dsvr"')
	parser.add_argument('-c', '--cuttlefish', default = 'cuttlefish',
		help = 'path to the cuttlefish tool for texture conversion')
	parser.add_argument('-j', '--multithread', default = False, action = 'store_true',
		help = 'multithread texture conversion')

	args = parser.parse_args()
	resources = VectorResources(args.cuttlefish)
	resources.loadFile(args.input)
	resources.save(args.output)
