""" Tools for reading Mac resource forks. """
from fontTools.misc.py23 import *
import struct
from fontTools.misc import sstruct
from collections import OrderedDict
from collections.abc import MutableMapping


class ResourceError(Exception):
	pass


class ResourceReader(MutableMapping):

	def __init__(self, fileOrPath):
		self._resources = OrderedDict()
		if hasattr(fileOrPath, 'read'):
			self.file = fileOrPath
		else:
			try:
				# try reading from the resource fork (only works on OS X)
				self.file = self.openResourceFork(fileOrPath)
				self._readFile()
				return
			except (ResourceError, IOError):
				# if it fails, use the data fork
				self.file = self.openDataFork(fileOrPath)
		self._readFile()

	@staticmethod
	def openResourceFork(path):
		if hasattr(path, "__fspath__"):  # support os.PathLike objects
			path = path.__fspath__()
		with open(path + '/..namedfork/rsrc', 'rb') as resfork:
			data = resfork.read()
		infile = BytesIO(data)
		infile.name = path
		return infile

	@staticmethod
	def openDataFork(path):
		with open(path, 'rb') as datafork:
			data = datafork.read()
		infile = BytesIO(data)
		infile.name = path
		return infile

	def _readFile(self):
		self._readHeaderAndMap()
		self._readTypeList()

	def _read(self, numBytes, offset=None):
		if offset is not None:
			try:
				self.file.seek(offset)
			except OverflowError:
				raise ResourceError("Failed to seek offset ('offset' is too large)")
			if self.file.tell() != offset:
				raise ResourceError('Failed to seek offset (reached EOF)')
		try:
			data = self.file.read(numBytes)
		except OverflowError:
			raise ResourceError("Cannot read resource ('numBytes' is too large)")
		if len(data) != numBytes:
			raise ResourceError('Cannot read resource (not enough data)')
		return data

	def _readHeaderAndMap(self):
		self.file.seek(0)
		headerData = self._read(ResourceForkHeaderSize)
		sstruct.unpack(ResourceForkHeader, headerData, self)
		# seek to resource map, skip reserved
		mapOffset = self.mapOffset + 22
		resourceMapData = self._read(ResourceMapHeaderSize, mapOffset)
		sstruct.unpack(ResourceMapHeader, resourceMapData, self)
		self.absTypeListOffset = self.mapOffset + self.typeListOffset
		self.absNameListOffset = self.mapOffset + self.nameListOffset

	def _readTypeList(self):
		absTypeListOffset = self.absTypeListOffset
		numTypesData = self._read(2, absTypeListOffset)
		self.numTypes, = struct.unpack('>H', numTypesData)
		absTypeListOffset2 = absTypeListOffset + 2
		for i in range(self.numTypes + 1):
			resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i
			resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset)
			item = sstruct.unpack(ResourceTypeItem, resTypeItemData)
			resType = tostr(item['type'], encoding='mac-roman')
			refListOffset = absTypeListOffset + item['refListOffset']
			numRes = item['numRes'] + 1
			resources = self._readReferenceList(resType, refListOffset, numRes)
			self._resources[resType] = resources

	def _readReferenceList(self, resType, refListOffset, numRes):
		resources = []
		for i in range(numRes):
			refOffset = refListOffset + ResourceRefItemSize * i
			refData = self._read(ResourceRefItemSize, refOffset)
			res = Resource(resType)
			res.decompile(refData, self)
			resources.append(res)
		return resources

	def __getitem__(self, resType):
		return self._resources[resType]

	def __delitem__(self, resType):
		del self._resources[resType]

	def __setitem__(self, resType, resources):
		self._resources[resType] = resources

	def __len__(self):
		return len(self._resources)

	def __iter__(self):
		return iter(self._resources)

	def keys(self):
		return self._resources.keys()

	@property
	def types(self):
		return list(self._resources.keys())

	def countResources(self, resType):
		"""Return the number of resources of a given type."""
		try:
			return len(self[resType])
		except KeyError:
			return 0

	def getIndices(self, resType):
		numRes = self.countResources(resType)
		if numRes:
			return list(range(1, numRes+1))
		else:
			return []

	def getNames(self, resType):
		"""Return list of names of all resources of a given type."""
		return [res.name for res in self.get(resType, []) if res.name is not None]

	def getIndResource(self, resType, index):
		"""Return resource of given type located at an index ranging from 1
		to the number of resources for that type, or None if not found.
		"""
		if index < 1:
			return None
		try:
			res = self[resType][index-1]
		except (KeyError, IndexError):
			return None
		return res

	def getNamedResource(self, resType, name):
		"""Return the named resource of given type, else return None."""
		name = tostr(name, encoding='mac-roman')
		for res in self.get(resType, []):
			if res.name == name:
				return res
		return None

	def close(self):
		if not self.file.closed:
			self.file.close()


class Resource(object):

	def __init__(self, resType=None, resData=None, resID=None, resName=None,
			     resAttr=None):
		self.type = resType
		self.data = resData
		self.id = resID
		self.name = resName
		self.attr = resAttr

	def decompile(self, refData, reader):
		sstruct.unpack(ResourceRefItem, refData, self)
		# interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct
		self.dataOffset, = struct.unpack('>L', bytesjoin([b"\0", self.dataOffset]))
		absDataOffset = reader.dataOffset + self.dataOffset
		dataLength, = struct.unpack(">L", reader._read(4, absDataOffset))
		self.data = reader._read(dataLength)
		if self.nameOffset == -1:
			return
		absNameOffset = reader.absNameListOffset + self.nameOffset
		nameLength, = struct.unpack('B', reader._read(1, absNameOffset))
		name, = struct.unpack('>%ss' % nameLength, reader._read(nameLength))
		self.name = tostr(name, encoding='mac-roman')


ResourceForkHeader = """
		> # big endian
		dataOffset:     L
		mapOffset:      L
		dataLen:        L
		mapLen:         L
"""

ResourceForkHeaderSize = sstruct.calcsize(ResourceForkHeader)

ResourceMapHeader = """
		> # big endian
		attr:              H
		typeListOffset:    H
		nameListOffset:    H
"""

ResourceMapHeaderSize = sstruct.calcsize(ResourceMapHeader)

ResourceTypeItem = """
		> # big endian
		type:              4s
		numRes:            H
		refListOffset:     H
"""

ResourceTypeItemSize = sstruct.calcsize(ResourceTypeItem)

ResourceRefItem = """
		> # big endian
		id:                h
		nameOffset:        h
		attr:              B
		dataOffset:        3s
		reserved:          L
"""

ResourceRefItemSize = sstruct.calcsize(ResourceRefItem)
