#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#	Copyright © 2014 dyknon
#
#	This file is part of Pylib-nicovideo.
#
#	Pylib-nicovideo is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program.  If not, see <http://www.gnu.org/licenses/>.

import distutils.core
import distutils.cmd
import distutils.command.build
import struct
import sys
import os

class TtfError(Exception):
	def __init__(self, val=""):
		self.value = val
	def __str__(self):
		return repr(self.value)

class TtfReader:
	def __init__(self, filehandle, offset=0):
		self.fh = filehandle
		self.offset = offset
		self.tables = {}
	
	def detect_type(self):
		self.fh.seek(self.offset, os.SEEK_SET)
		mn = self.fh.read(4)
		if mn == b"\x00\x01\x00\x00":
			return "ttf"
		elif mn == b"ttcf":
			if self.fh.read(4) == b"\x00\x01\x00\x00":
				return "ttc"
		return None
	
	def read_ttc_head(self):
		self.fh.seek(self.offset, os.SEEK_SET)
		if self.fh.read(8) != b"ttcf\x00\x01\x00\x00":
			raise TtfError("TTCファイルヘッダ読み込みエラー")
		self.tttable = []
		numfn = struct.unpack(">L", self.fh.read(4))[0]
		for i in range(numfn):
			self.tttable.append(struct.unpack(">L", self.fh.read(4))[0])
		return self.tttable

	def read_ttf_head(self):
		self.fh.seek(self.offset, os.SEEK_SET)
		if self.fh.read(4) != b"\x00\x01\x00\x00":
			raise TtfError("TTFファイルヘッダ読み込みエラー")
		(num_tables, search_range, entry_selector, range_shift) \
			= struct.unpack(">HHHH", self.fh.read(8))
		self.table_list = {}
		for i in range(num_tables):
			tag = self.fh.read(4).decode("ascii")
			(cksum, offset, length) = struct.unpack(">LLL", self.fh.read(12))
			self.table_list[tag] = {"s": cksum, "o": offset, "l": length}
		return self.table_list

	def check_ttf_head(self):
		try:
			self.table_list
		except AttributeError:
			self.read_ttf_head()

	def get_table_head(self):
		if not "head" in self.tables:
			self.read_table_head()
		return self.tables["head"]

	def read_table_head(self):
		self.check_ttf_head()
		self.fh.seek(self.table_list["head"]["o"], os.SEEK_SET)
		(version, font_revidion, check_sum_adjustment, magic_number,
			flags, units_per_em, created, modified,
			x_min, y_min, x_max, y_max, mac_style, lowest_rec_ppem,
			font_direction_hint, index_to_loc_format, glyph_data_format) \
			= struct.unpack(">llLLHHqqhhhhHHhhh", self.fh.read(54));
		version /= 0x10000
		font_revidion /= 0x10000
		self.tables["head"] = {
			"version": version,
			"fontRevidion": font_revidion,
			"checkSumAdjustment": check_sum_adjustment,
			"magicNumber": magic_number,
			"flags": flags,
			"unitsPerEm": units_per_em,
			"created": created,
			"modified": modified,
			"xMin": x_min,
			"yMin": y_min,
			"xMax": x_max,
			"yMax": y_max,
			"macStyle": mac_style,
			"lowestRecPPEM": lowest_rec_ppem,
			"fontDirectionHint": font_direction_hint,
			"indexToLocFormat": index_to_loc_format,
			"glyphDataFormat": glyph_data_format
		}
		return self.tables["head"]

	def get_table_loca(self):
		if not "loca" in self.tables:
			self.read_table_loca()
		return self.tables["loca"]

	def read_table_loca(self):
		self.check_ttf_head()
		if not "head" in self.tables:
			self.read_table_head()
		if not "maxp" in self.tables:
			self.read_table_maxp()
		self.fh.seek(self.table_list["loca"]["o"], os.SEEK_SET)
		if self.tables["head"]["indexToLocFormat"] == 0:
			table_format = struct.Struct(">H")
		elif self.tables["head"]["indexToLocFormat"] == 1:
			table_format = struct.Struct(">L")
		else:
			raise TtfError("headテーブルのindexToLocFormat値が不正。")
		self.tables["loca"] = []
		for i in range(self.tables["maxp"]["numGlyphs"]+1):
			offset = table_format.unpack(self.fh.read(table_format.size))
			self.tables["loca"].append(offset)
		return self.tables["loca"]

	def get_glyph_from_index(self, index):
		self.check_ttf_head()
		self.fh.seek(self.table_list["glyf"]["o"]+index, os.SEEK_SET)
		(number_of_contours, x_min, y_min, x_max, y_max) \
			= struct.unpack(">hhhhh", self.fh.read(10))
		return {
			"numberOfContours": number_of_contours,
			"xMin": x_min,
			"yMin": y_min,
			"xMax": x_max,
			"yMax": y_max
		}

	def get_glyph_from_no(self, no):
		if "glyf" in self.tables and no in self.tables["glyf"]:
			return self.tables["glyf"][no]
		if not "glyf" in self.tables:
			self.tables["glyf"] = {}
		if not "loca" in self.tables:
			self.read_table_loca()
		glyph = self.get_glyph_from_index(self.tables["loca"][no])
		self.tables["glyf"][no] = glyph
		return glyph

	def get_glyph_from_char(self, char):
		if not "cmap" in self.tables:
			self.read_table_cmap()
		return self.get_glyph_from_no(self.tables["cmap"][char])

	def get_table_glyf(self):
		if not "glyf" in self.tables:
			self.read_table_glyf()
		return self.tables["glyf"]

	def read_table_glyf(self):
		if not "maxp" in self.tables:
			self.read_table_maxp()
		for i in range(self.tables["maxp"]["numGlyphs"]+1):
			self.get_glyph_from_no(i)
		return self.tables["glyf"]

	def get_table_cmap(self):
		if not "cmap" in self.tables:
			self.read_table_cmap()
		return self.tables["cmap"]

	def read_table_cmap(self):		#手抜きとかそういうのじゃない。無理。
		self.check_ttf_head()
		self.fh.seek(self.table_list["cmap"]["o"], os.SEEK_SET)
		(version, num_tables) = struct.unpack(">HH", self.fh.read(4));
		if version != 0:
			raise TtfError("cmapテーブルのバージョンがあたらしすぎまっす。")
		encoding_records = []
		maxscore = 0
		no = -1
		for i in range(num_tables):
			(platform_id, encoding_id, offset) \
				= struct.unpack(">HHL", self.fh.read(8))
			score = 0
			if platform_id == 3:
				if encoding_id == 0:
					score += 1
				elif encoding_id == 1:
					score += 3
			if maxscore < score:
				maxscore = score
				no = i
			encoding_records.append({
				"platformID": platform_id,
				"encodingID": encoding_id,
				"offset": offset
			})
		if no < 0:
			raise TtfError("文字マップのエンコード方式に非対応のものしかありません")
		encoding = None
		if encoding_records[no]["platformID"] == 3:
			if encoding_records[no]["encodingID"] == 0:
				encoding = "ascii"
			if encoding_records[no]["encodingID"] == 1:
				encoding = "utf-16be"
			if encoding_records[no]["encodingID"] == 2:
				encoding = "sjis"
		if encoding == None:
			raise TtfError("文字マップのエンコード方式に非対応のものしかありません")
		self.fh.seek(self.table_list["cmap"]["o"]
			+ encoding_records[no]["offset"], os.SEEK_SET)
		maptype = struct.unpack(">H", self.fh.read(2))[0]
		mapdict = {}
		if maptype == 0:				#要修正: デコードの仕方ダメ
			(length, language) = struct.unpack(">HH", self.fh.read(4))
			for i in range(256):
				code = struct.pack(">L", i).decode(encoding, "replace")
				mapdict[code] = struct.unpack(">B", self.fh.read(1))[0]
		elif maptype == 2:
			(length, language) = struct.unpack(">HH", self.fh.read(4))
			sub_header_keys = []
			for i in range(256):
				sub_header_keys.append(struct.unpack(">H", self.fh.read(2))[0])
			start_of_sub_header = self.fh.tell()
			for i in range(256):
				self.fh.seek(start_of_sub_header + sub_header_keys[i], os.SEEK_SET)
				(first_code, entry_count, id_delta, id_range_offset) \
					= struct.unpack(">HHsH", self.fh.read(8))
				if first_code + entry_count >= 256:
					continue
				self.fh.seek(id_range_offset, SEEK_CUR)
				for j in range(entry_count):
					glyph_index = struct.unpack(">H", self.fh.read(2))[0]
					if glyph_index == 0:
						continue
					glyph_index += id_delta
					if i == 0:
						code = j + first_code
					else:
						code = (i << 8)|((j+first_code) & 0x0f)
					code = struct.pack("B", code).decode(encoding, "replace")
					mapdict[code] = glyph_index
		elif maptype == 4:
			(length, language, seg_count_x2, search_range, entry_selector,
				range_shift) = struct.unpack(">HHHHHH", self.fh.read(12))
			seg_count = int(seg_count_x2 / 2)
			end_count = []
			for i in range(seg_count):
				end_count.append(struct.unpack(">H", self.fh.read(2))[0])
			reserved_pad = struct.unpack(">H", self.fh.read(2))[0]
			start_count = []
			for i in range(seg_count):
				start_count.append(struct.unpack(">H", self.fh.read(2))[0])
			id_delta = []
			for i in range(seg_count):
				id_delta.append(struct.unpack(">h", self.fh.read(2))[0])
			id_range_offset = []
			for i in range(seg_count):
				id_range_offset.append(struct.unpack(">H", self.fh.read(2))[0])
			start_of_glyph_id_array = self.fh.tell()
			for i in range(seg_count):
				start = start_count[i]
				end = end_count[i]
				delta = id_delta[i]
				range_offset = id_range_offset[i]
				if start == 65535 or end == 65535:
					continue
				for j in range(start, end+1):
					if range_offset == 0:
						code = struct.pack(">H", j)
						code = code.decode(encoding, "replace")
						mapdict[code] = (j + delta) % 65536
					else:
						self.fh.seek(
							start_of_glyph_id_array
								+ int((range_offset/2+j-start+i-seg_count)*2),
							os.SEEK_SET)
						glyph_index = struct.unpack(">H", self.fh.read(2))[0]
						if glyph_index == 0:
							continue
						code = struct.pack(">H", j)
						code = code.decode(encoding, "replace")
						mapdict[code] = (glyph_index + delta) % 65536
		else:
			raise TtfError("ID{}の文字マップには対応していません".format(maptype))
		self.tables["cmap"] = mapdict
		return self.tables["cmap"]

	def get_table_maxp(self):
		if not "maxp" in self.tables:
			self.read_table_maxp()
		return self.tables["maxp"]

	def read_table_maxp(self):
		self.check_ttf_head()
		self.fh.seek(self.table_list["maxp"]["o"], os.SEEK_SET)
		self.tables["maxp"] = {}
		version = struct.unpack(">L", self.fh.read(4))[0]
		if version == 0x00005000:
			self.tables["maxp"]["numGlyphs"] \
				= struct.unpack(">S", self.fh.read(2))[0]
		elif version == 0x00010000:		#手抜き(v1.0で追加されたのを無視する)
			self.tables["maxp"]["numGlyphs"] \
				= struct.unpack(">S", self.fh.read(2))[0]
		else:
			raise TtfError("maxpテーブルのバージョンが非対応なやつです")

	def get_table_hhea(self):
		if not "hhea" in self.tables:
			self.read_table_hhea()
		return self.tables["hhea"]

	def read_table_hhea(self):
		self.check_ttf_head()
		self.fh.seek(self.table_list["hhea"]["o"], os.SEEK_SET)
		(vesion, ascender, descender, line_gap, advance_width_max,
		min_left_side_bearing, min_right_side_bearing, x_max_extent,
		caret_slope_rise, caret_slope_run, caret_offset,
		zero0, zero1, zero2, zero3, metric_data_format, number_of_h_metrics) \
			= struct.unpack(">LhhhHhhhhhhhhhhhH", self.fh.read(36));
		self.tables["hhea"] = {
			"vesion": vesion,
			"ascender": ascender,
			"descender": descender,
			"lineGap": line_gap,
			"advanceWidthMax": advance_width_max,
			"minLeftSideBearing": min_left_side_bearing,
			"minRightSideBearing": min_right_side_bearing,
			"xMaxExtent": x_max_extent,
			"caretSlopeRise": caret_slope_rise,
			"caretSlopeRun": caret_slope_run,
			"caretOffset": caret_offset,
			"metricDataFormat": metric_data_format,
			"numberOfHMetrics": number_of_h_metrics
		}
		return self.tables["hhea"]

	def get_table_hmtx(self):
		if not "hmtx" in self.tables:
			self.read_table_hmtx()
		return self.tables["hmtx"]

	def read_table_hmtx(self):		#手抜き
		self.check_ttf_head()
		if not "hhea" in self.tables:
			self.read_table_hhea()
		self.fh.seek(self.table_list["hmtx"]["o"], os.SEEK_SET)
		long_hor_metric = []
		for i in range(self.tables["hhea"]["numberOfHMetrics"]):
			(advance_width, lsb) = struct.unpack(">Hh", self.fh.read(4))
			long_hor_metric.append({"advanceWidth": advance_width, "lsb": lsb})
		self.tables["hmtx"] = long_hor_metric
		return long_hor_metric

	def get_charwidth_from_no(self, no):
		if not "hmtx" in self.tables:
			self.read_table_hmtx()
		if len(self.tables["hmtx"]) <= no:
			return None
		return self.tables["hmtx"][no]["advanceWidth"]

	def get_charwidth_from_char(self, char):
		if not "cmap" in self.tables:
			self.read_table_cmap()
		return self.get_charwidth_from_no(self.tables["cmap"][char])

	def get_table_name(self):
		if not "name" in self.tables:
			self.read_table_name()
		return self.tables["name"]

	def read_table_name(self):		#だいぶ手抜き
		self.check_ttf_head()
		self.fh.seek(self.table_list["name"]["o"], os.SEEK_SET)
		(format_num, count, string_offset) \
			= struct.unpack(">HHH", self.fh.read(6))
		name_record = []
		for i in range(count):
			(platform_id, encoding_id, language_id, name_id, length, offset) \
				= struct.unpack(">HHHHHH", self.fh.read(12))
			name_record.append({
				"platformID": platform_id,
				"encodingID": encoding_id,
				"languageID": language_id,
				"nameID": name_id,
				"length": length,
				"offset": offset
			})
		storagepos = self.table_list["name"]["o"] + string_offset
		self.tables["name"] = []
		for nr in name_record:			#要修正: encID=0はAsciiだよ。雑過ぎ。
			encoding = None
			if nr["platformID"] == 3:
				if nr["encodingID"] == 0:
					encoding = "utf-16be"
				elif nr["encodingID"] == 1:
					encoding = "utf-16be"
				elif nr["encodingID"] == 2:
					encoding = "sjis"
			if not encoding:
				continue
			self.fh.seek(storagepos + nr["offset"], os.SEEK_SET)
			string = self.fh.read(nr["length"]).decode(encoding, "replace")
			nr.update({"string": string})
			self.tables["name"].append(nr)
		return self.tables["name"]

	def get_font_name(self):
		if not "name" in self.tables:
			self.read_table_name()
		fontname = None
		maxscore = -1
		for name in self.tables["name"]:
			score = 0
			if name["nameID"] == 4:
				score += 3
			if name["languageID"] == 0x0411:
				score += 2
			if name["encodingID"] == 1:
				score += 1
			if maxscore < score:
				maxscore = score
				fontname = name["string"]
		return fontname

	def has_name(self, rqname):
		if not "name" in self.tables:
			self.read_table_name()
		for name in self.tables["name"]:
			if name["nameID"] == 4:
				if name["string"] == rqname:
					return True
		return False

# ** フォント優先順位テーブルについて **
#適切なフォントが検出できない時のためにいくつか自動検出の候補を用意しておく。
#自分の手元にあったフォントからテキトーに選んだだけ。

#半角文字用フォントの優先順位。HelveticaというやつはArialと幅が同じらしい。
HWalphanumeric_priority = [
	"Arial", "Helvetica", "DejaVu Sans", "VL PGothic Regular"
]
#ゴシック体用フォントの優先順位。ＭＳ Ｐゴシック以外はテキトーに並べただけ。
gothic_priority = [
	"ＭＳ Ｐゴシック", "VL PGothic Regular"
]
gothic_replacer = ""
#明朝体用のフォントの優先順位。SimSun以外はテキトーに並べただけ。最後はゴシック
mincho_priority = [
	"SimSun", "VL PGothic Regular"
]
mincho_replacer = "\u02c9\u02ca\u02cb\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588\u2589\u258a\u258b\u258c\u258d\u258e\u258f\u311a"
#丸文字体用のフォント優先順位。Gulim以外はただのゴシック
marumoji_priority = [
	"Gulim", "ＭＳ Ｐゴシック", "VL PGothic Regular"
]
marumoji_replacer = "\u2660\u2661\u2663\u2664\u2665\u2667\u2668\u2669\u266c\uadf8"

hwfont_reader = None
fwfont_readers = []
fwfont_replacers = {}

def gen_fontwidth_py(outputfile):
	#自動設定はWin以外は今のところ非対応。
	fontfiles = []
	fontdirs = []
	if os.path.isdir("fonts"):
		fontdirs.append("fonts")
	if os.name == "nt":
		if os.path.isdir("C:\\Windows\\Fonts\\"):
			fontdirs.append("C:\\Windows\\Fonts\\")
	for fontdir in fontdirs:
		infontdir = os.listdir(fontdir)
		for filename in infontdir:
			filename = os.path.join(fontdir, filename)
			if os.path.isfile(filename) and os.access(filename, os.R_OK):
				fontfiles.append(filename)
	fonts = []
	for fontfile in fontfiles:
		fh = open(fontfile, "br")
		headreader = TtfReader(fh)
		filetype = headreader.detect_type()
		if filetype == "ttf":
			fonts.append(headreader)
		elif filetype == "ttc":
			for offset in headreader.read_ttc_head():
				fonts.append(TtfReader(fh, offset))
	font_can_HW = None
	font_can_gothic = None
	font_can_mincho = None
	font_can_marumoji = None
	for rqname in reversed(HWalphanumeric_priority):
		for font in fonts:
			if font.has_name(rqname):
				font_can_HW = font
	for rqname in reversed(gothic_priority):
		for font in fonts:
			if font.has_name(rqname):
				font_can_gothic = font
	for rqname in reversed(mincho_priority):
		for font in fonts:
			if font.has_name(rqname):
				font_can_mincho = font
	for rqname in reversed(marumoji_priority):
		for font in fonts:
			if font.has_name(rqname):
				font_can_marumoji = font
	if not(font_can_HW and font_can_gothic and
		font_can_mincho and font_can_marumoji):
		print("適切なフォントをfonts/に入れてください。(README参照)")
		raise TtfError("必要なフォントを自動検出できんかったよ")
	hwfont_reader = font_can_HW
	fwfont_readers = [font_can_gothic, font_can_mincho, font_can_marumoji]
	fwfont_replacers = {
		font_can_gothic.get_font_name(): gothic_replacer,
		font_can_mincho.get_font_name(): mincho_replacer,
		font_can_marumoji.get_font_name(): marumoji_replacer
	}

	outputfile.write("#Generated by nicovideo-tools")
	outputfile.write("\n")
	outputfile.write("font_HWalphanumeric = \"{}\"".format(hwfont_reader.get_font_name()))
	outputfile.write("\n")
	nametable = []
	for tr in fwfont_readers:
		nametable.append("\"{}\"".format(tr.get_font_name()))
	outputfile.write("font_priority = [{}]".format(",".join(nametable)))
	outputfile.write("\n")
	outputfile.write("font_change_chars = {")
	outputfile.write("\n")
	for key in fwfont_replacers.keys():
		line = "\t\"{}\": \"".format(key)
		for char in fwfont_replacers[key]:
			line += "\\U{:0>8x}".format(ord(char))
		line += "\","
		outputfile.write(line)
		outputfile.write("\n")
	outputfile.write("\t\"\": \"\"")
	outputfile.write("\n")
	outputfile.write("}")
	outputfile.write("\n")
	outputfile.write("fontlist = {")
	outputfile.write("\n")
	for tr in fwfont_readers:
		outputfile.write("\t\"" + tr.get_font_name() + "\": {")
		outputfile.write("\n")
		outputfile.write("\t\t\"h\": {},".format(tr.get_table_head()["unitsPerEm"]))
		outputfile.write("\n")
		outputfile.write("\t\t\"w\": {")
		outputfile.write("\n")
		charmap = tr.get_table_cmap()
		for char in charmap.keys():
			width = tr.get_charwidth_from_char(char)
			if width != None:
				outputfile.write("\t\t\t\"\\U{:0>8x}\": {},".format(ord(char), width))
				outputfile.write("\n")
		outputfile.write("\t\t\t\"\": 0")
		outputfile.write("\n")
		outputfile.write("\t\t}")
		outputfile.write("\n")
		outputfile.write("\t},")
		outputfile.write("\n")

	outputfile.write("\t\"" + hwfont_reader.get_font_name() + "\": {")
	outputfile.write("\n")
	outputfile.write("\t\t\"h\": {},".format(hwfont_reader.get_table_head()["unitsPerEm"]))
	outputfile.write("\n")
	outputfile.write("\t\t\"w\": {")
	outputfile.write("\n")
	charmap = hwfont_reader.get_table_cmap()
	for char in charmap.keys():
		width = hwfont_reader.get_charwidth_from_char(char)
		if width != None:
			outputfile.write("\t\t\t\"\\U{:0>8x}\": {},".format(ord(char), width))
			outputfile.write("\n")
	outputfile.write("\t\t\t\"\": 0")
	outputfile.write("\n")
	outputfile.write("\t\t}")
	outputfile.write("\n")
	outputfile.write("\t}")
	outputfile.write("\n")
	outputfile.write("}")
	outputfile.write("\n")

class build_fontwidth_py(distutils.cmd.Command):
	description = "\"build\" fontwidth.py"

	user_options = [("build-lib=", "d", "directory to \"build\" to")]
	boolean_options = []

	def initialize_options(self):
		self.build_lib = None

	def finalize_options(self):
		self.set_undefined_options("build", ('build_lib', 'build_lib'))

	def run(self):
		self.mkpath(os.path.join(self.build_lib, "nicovideo"))
		outfile = os.path.join(self.build_lib, "nicovideo", "fontwidth.py")
		outfilehandle = open(outfile, mode="w", encoding="utf-8")
		outfilehandle.write("# -*- coding: utf-8 -*-\n")
		gen_fontwidth_py(outfilehandle)
		outfilehandle.close()

class my_build(distutils.command.build.build):
	def run(self):
		self.run_command("build_fontwidth_py")
		distutils.command.build.build.run(self)

distutils.core.setup(	name="pylib-nicovideo",
						version="0.3.0.0",
						description="ニコニコにアクセスしたりできる。",
						author="dyknon",
						author_email="dyknon@users.sourceforge.jp",
						url="http://sourceforge.jp/users/dyknon/",
						packages=["nicovideo"],
						cmdclass={
							"build": my_build,
							"build_fontwidth_py": build_fontwidth_py
						}
)
