import logging
import os
import tempfile
import shutil
import unittest
from io import open
from .testSupport import getDemoFontGlyphSetPath
from fontTools.ufoLib.glifLib import (
	GlyphSet, glyphNameToFileName, readGlyphFromString, writeGlyphToString,
)
from fontTools.ufoLib.errors import GlifLibError, UnsupportedGLIFFormat, UnsupportedUFOFormat
from fontTools.misc.etree import XML_DECLARATION
from fontTools.pens.recordingPen import RecordingPointPen
import pytest

GLYPHSETDIR = getDemoFontGlyphSetPath()


class GlyphSetTests(unittest.TestCase):

	def setUp(self):
		self.dstDir = tempfile.mktemp()
		os.mkdir(self.dstDir)

	def tearDown(self):
		shutil.rmtree(self.dstDir)

	def testRoundTrip(self):
		import difflib
		srcDir = GLYPHSETDIR
		dstDir = self.dstDir
		src = GlyphSet(srcDir, ufoFormatVersion=2, validateRead=True, validateWrite=True)
		dst = GlyphSet(dstDir, ufoFormatVersion=2, validateRead=True, validateWrite=True)
		for glyphName in src.keys():
			g = src[glyphName]
			g.drawPoints(None)  # load attrs
			dst.writeGlyph(glyphName, g, g.drawPoints)
		# compare raw file data:
		for glyphName in sorted(src.keys()):
			fileName = src.contents[glyphName]
			with open(os.path.join(srcDir, fileName), "r") as f:
				org = f.read()
			with open(os.path.join(dstDir, fileName), "r") as f:
				new = f.read()
			added = []
			removed = []
			for line in difflib.unified_diff(
					org.split("\n"), new.split("\n")):
				if line.startswith("+ "):
					added.append(line[1:])
				elif line.startswith("- "):
					removed.append(line[1:])
			self.assertEqual(
				added, removed,
				"%s.glif file differs after round tripping" % glyphName)

	def testContentsExist(self):
		with self.assertRaises(GlifLibError):
			GlyphSet(
				self.dstDir,
				ufoFormatVersion=2,
				validateRead=True,
				validateWrite=True,
				expectContentsFile=True,
			)

	def testRebuildContents(self):
		gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
		contents = gset.contents
		gset.rebuildContents()
		self.assertEqual(contents, gset.contents)

	def testReverseContents(self):
		gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
		d = {}
		for k, v in gset.getReverseContents().items():
			d[v] = k
		org = {}
		for k, v in gset.contents.items():
			org[k] = v.lower()
		self.assertEqual(d, org)

	def testReverseContents2(self):
		src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
		dst = GlyphSet(self.dstDir, validateRead=True, validateWrite=True)
		dstMap = dst.getReverseContents()
		self.assertEqual(dstMap, {})
		for glyphName in src.keys():
			g = src[glyphName]
			g.drawPoints(None)  # load attrs
			dst.writeGlyph(glyphName, g, g.drawPoints)
		self.assertNotEqual(dstMap, {})
		srcMap = dict(src.getReverseContents())  # copy
		self.assertEqual(dstMap, srcMap)
		del srcMap["a.glif"]
		dst.deleteGlyph("a")
		self.assertEqual(dstMap, srcMap)

	def testCustomFileNamingScheme(self):
		def myGlyphNameToFileName(glyphName, glyphSet):
			return "prefix" + glyphNameToFileName(glyphName, glyphSet)
		src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
		dst = GlyphSet(self.dstDir, myGlyphNameToFileName, validateRead=True, validateWrite=True)
		for glyphName in src.keys():
			g = src[glyphName]
			g.drawPoints(None)  # load attrs
			dst.writeGlyph(glyphName, g, g.drawPoints)
		d = {}
		for k, v in src.contents.items():
			d[k] = "prefix" + v
		self.assertEqual(d, dst.contents)

	def testGetUnicodes(self):
		src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
		unicodes = src.getUnicodes()
		for glyphName in src.keys():
			g = src[glyphName]
			g.drawPoints(None)  # load attrs
			if not hasattr(g, "unicodes"):
				self.assertEqual(unicodes[glyphName], [])
			else:
				self.assertEqual(g.unicodes, unicodes[glyphName])


class FileNameTest:

	def test_default_file_name_scheme(self):
		assert glyphNameToFileName("a", None) == "a.glif"
		assert glyphNameToFileName("A", None) == "A_.glif"
		assert glyphNameToFileName("Aring", None) == "A_ring.glif"
		assert glyphNameToFileName("F_A_B", None) == "F__A__B_.glif"
		assert glyphNameToFileName("A.alt", None) == "A_.alt.glif"
		assert glyphNameToFileName("A.Alt", None) == "A_.A_lt.glif"
		assert glyphNameToFileName(".notdef", None) == "_notdef.glif"
		assert glyphNameToFileName("T_H", None) =="T__H_.glif"
		assert glyphNameToFileName("T_h", None) =="T__h.glif"
		assert glyphNameToFileName("t_h", None) =="t_h.glif"
		assert glyphNameToFileName("F_F_I", None) == "F__F__I_.glif"
		assert glyphNameToFileName("f_f_i", None) == "f_f_i.glif"
		assert glyphNameToFileName("AE", None) == "A_E_.glif"
		assert glyphNameToFileName("Ae", None) == "A_e.glif"
		assert glyphNameToFileName("ae", None) == "ae.glif"
		assert glyphNameToFileName("aE", None) == "aE_.glif"
		assert glyphNameToFileName("a.alt", None) == "a.alt.glif"
		assert glyphNameToFileName("A.aLt", None) == "A_.aL_t.glif"
		assert glyphNameToFileName("A.alT", None) == "A_.alT_.glif"
		assert glyphNameToFileName("Aacute_V.swash", None) == "A_acute_V_.swash.glif"
		assert glyphNameToFileName(".notdef", None) == "_notdef.glif"
		assert glyphNameToFileName("con", None) == "_con.glif"
		assert glyphNameToFileName("CON", None) == "C_O_N_.glif"
		assert glyphNameToFileName("con.alt", None) == "_con.alt.glif"
		assert glyphNameToFileName("alt.con", None) == "alt._con.glif"

	def test_conflicting_case_insensitive_file_names(self, tmp_path):
		src = GlyphSet(GLYPHSETDIR)
		dst = GlyphSet(tmp_path)
		glyph = src["a"]

		dst.writeGlyph("a", glyph)
		dst.writeGlyph("A", glyph)
		dst.writeGlyph("a_", glyph)
		dst.deleteGlyph("a_")
		dst.writeGlyph("a_", glyph)
		dst.writeGlyph("A_", glyph)
		dst.writeGlyph("i_j", glyph)

		assert dst.contents == {
			'a': 'a.glif',
			'A': 'A_.glif',
			'a_': 'a_000000000000001.glif',
			'A_': 'A__.glif',
			'i_j': 'i_j.glif',
		}

		# make sure filenames are unique even on case-insensitive filesystems
		assert len({fileName.lower() for fileName in dst.contents.values()}) == 5


class _Glyph:
	pass


class ReadWriteFuncTest:

	def test_roundtrip(self):
		glyph = _Glyph()
		glyph.name = "a"
		glyph.unicodes = [0x0061]

		s1 = writeGlyphToString(glyph.name, glyph)

		glyph2 = _Glyph()
		readGlyphFromString(s1, glyph2)
		assert glyph.__dict__ == glyph2.__dict__

		s2 = writeGlyphToString(glyph2.name, glyph2)
		assert s1 == s2

	def test_xml_declaration(self):
		s = writeGlyphToString("a", _Glyph())
		assert s.startswith(XML_DECLARATION % "UTF-8")

	def test_parse_xml_remove_comments(self):
		s = b"""<?xml version='1.0' encoding='UTF-8'?>
		<!-- a comment -->
		<glyph name="A" format="2">
			<advance width="1290"/>
			<unicode hex="0041"/>
			<!-- another comment -->
		</glyph>
		"""

		g = _Glyph()
		readGlyphFromString(s, g)

		assert g.name == "A"
		assert g.width == 1290
		assert g.unicodes == [0x0041]

	def test_read_unsupported_format_version(self, caplog):
		s = """<?xml version='1.0' encoding='utf-8'?>
		<glyph name="A" format="0" formatMinor="0">
			<advance width="500"/>
			<unicode hex="0041"/>
		</glyph>
		"""

		with pytest.raises(UnsupportedGLIFFormat):
			readGlyphFromString(s, _Glyph())  # validate=True by default

		with pytest.raises(UnsupportedGLIFFormat):
			readGlyphFromString(s, _Glyph(), validate=True)

		caplog.clear()
		with caplog.at_level(logging.WARNING, logger="fontTools.ufoLib.glifLib"):
			readGlyphFromString(s, _Glyph(), validate=False)

		assert len(caplog.records) == 1
		assert "Unsupported GLIF format" in caplog.text
		assert "Assuming the latest supported version" in caplog.text

	def test_read_allow_format_versions(self):
		s = """<?xml version='1.0' encoding='utf-8'?>
		<glyph name="A" format="2">
			<advance width="500"/>
			<unicode hex="0041"/>
		</glyph>
		"""

		# these two calls are are equivalent
		readGlyphFromString(s, _Glyph(), formatVersions=[1, 2])
		readGlyphFromString(s, _Glyph(), formatVersions=[(1, 0), (2, 0)])

		# if at least one supported formatVersion, unsupported ones are ignored
		readGlyphFromString(s, _Glyph(), formatVersions=[(2, 0), (123, 456)])

		with pytest.raises(
			ValueError,
			match="None of the requested GLIF formatVersions are supported"
		):
			readGlyphFromString(s, _Glyph(), formatVersions=[0, 2001])

		with pytest.raises(GlifLibError, match="Forbidden GLIF format version"):
			readGlyphFromString(s, _Glyph(), formatVersions=[1])

	def test_read_ensure_x_y(self):
		"""Ensure that a proper GlifLibError is raised when point coordinates are
		missing, regardless of validation setting."""

		s = """<?xml version='1.0' encoding='utf-8'?>
		<glyph name="A" format="2">
			<outline>
				<contour>
					<point x="545" y="0" type="line"/>
					<point x="638" type="line"/>
				</contour>
			</outline>
		</glyph>
		"""
		pen = RecordingPointPen() 

		with pytest.raises(GlifLibError, match="Required y attribute"):
			readGlyphFromString(s, _Glyph(), pen)

		with pytest.raises(GlifLibError, match="Required y attribute"):
			readGlyphFromString(s, _Glyph(), pen, validate=False)

def test_GlyphSet_unsupported_ufoFormatVersion(tmp_path, caplog):
	with pytest.raises(UnsupportedUFOFormat):
		GlyphSet(tmp_path, ufoFormatVersion=0)
	with pytest.raises(UnsupportedUFOFormat):
		GlyphSet(tmp_path, ufoFormatVersion=(0, 1))


def test_GlyphSet_writeGlyph_formatVersion(tmp_path):
	src = GlyphSet(GLYPHSETDIR)
	dst = GlyphSet(tmp_path, ufoFormatVersion=(2, 0))
	glyph = src["A"]

	# no explicit formatVersion passed: use the more recent GLIF formatVersion
	# that is supported by given ufoFormatVersion (GLIF 1 for UFO 2)
	dst.writeGlyph("A", glyph)
	glif = dst.getGLIF("A")
	assert b'format="1"' in glif
	assert b'formatMinor' not in glif  # omitted when 0

	# explicit, unknown formatVersion
	with pytest.raises(UnsupportedGLIFFormat):
		dst.writeGlyph("A", glyph, formatVersion=(0, 0))

	# explicit, known formatVersion but unsupported by given ufoFormatVersion
	with pytest.raises(
		UnsupportedGLIFFormat,
		match="Unsupported GLIF format version .*for UFO format version",
	):
		dst.writeGlyph("A", glyph, formatVersion=(2, 0))
