using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

#pragma warning disable IDE0049
namespace Cryville.Common.Font {
	public abstract class FontTable<T> {
		protected UInt32 Offset { get; private set; }
		protected BinaryReader Reader { get; private set; }

		protected FontTable(BinaryReader reader, UInt32 offset) {
			Reader = reader;
			Offset = offset;
			reader.BaseStream.Position = offset;
		}
		public abstract IReadOnlyList<T> GetItems();
	}
	public abstract class FontTable<T, U> : FontTable<T> {
		protected FontTable(BinaryReader reader, UInt32 offset) : base(reader, offset) { }
		public abstract U GetSubTable(T item);
	}
	public sealed class TTCHeader : FontTable<UInt32, TableDirectory> {
		readonly String ttcTag;
		readonly UInt16 majorVersion;
		readonly UInt16 minorVersion;
		readonly UInt32 numFonts;
		readonly List<UInt32> tableDirectoryOffsets = new List<UInt32>();
#pragma warning disable IDE0052 // Reserved
		readonly String dsigTag;
		readonly UInt32 dsigLength;
		readonly UInt32 dsigOffset;
#pragma warning restore IDE0052 // Reserved
		public TTCHeader(BinaryReader reader, UInt32 offset) : base(reader, offset) {
			ttcTag = reader.ReadTag();
			if (ttcTag != "ttcf") throw new NotSupportedException();
			majorVersion = reader.ReadUInt16();
			minorVersion = reader.ReadUInt16();
			if (minorVersion != 0) throw new NotSupportedException();
			numFonts = reader.ReadUInt32();
			for (UInt32 i = 0; i < numFonts; i++) tableDirectoryOffsets.Add(reader.ReadUInt32());
			if (majorVersion == 2) {
				dsigTag = reader.ReadTag();
				dsigLength = reader.ReadUInt32();
				dsigOffset = reader.ReadUInt32();
			}
		}
		public override IReadOnlyList<UInt32> GetItems() {
			return tableDirectoryOffsets;
		}
		public override TableDirectory GetSubTable(UInt32 item) {
			var i = item;
			return new TableDirectory(Reader, i);
		}
	}
	public sealed class TableDirectory : FontTable<TableRecord, object> {
		readonly UInt32 sfntVersion;
		readonly UInt16 numTables;
#pragma warning disable IDE0052 // Reserved
		readonly UInt16 searchRange;
		readonly UInt16 entrySelector;
		readonly UInt16 rangeShift;
#pragma warning restore IDE0052 // Reserved
		readonly List<TableRecord> tableRecords = new List<TableRecord>();
		public TableDirectory(BinaryReader reader, UInt32 offset) : base(reader, offset) {
			sfntVersion = reader.ReadUInt32();
			if (sfntVersion != 0x00010000 && sfntVersion != 0x4F54544F &&
				sfntVersion != 0x74727565 && sfntVersion != 0x74797031) throw new NotSupportedException();
			numTables = reader.ReadUInt16();
			searchRange = reader.ReadUInt16();
			entrySelector = reader.ReadUInt16();
			rangeShift = reader.ReadUInt16();
			for (int i = 0; i < numTables; i++)
				tableRecords.Add(new TableRecord {
					tableTag = reader.ReadTag(),
					checksum = reader.ReadUInt32(),
					offset = reader.ReadUInt32(),
					length = reader.ReadUInt32(),
				});
		}
		public override IReadOnlyList<TableRecord> GetItems() {
			return tableRecords;
		}
		public override object GetSubTable(TableRecord item) {
			switch (item.tableTag) {
				case "name": return new NameTable(Reader, item.offset);
				case "meta": return new MetaTable(Reader, item.offset);
				default: throw new NotImplementedException();
			}
		}
	}
	public struct TableRecord {
		public string tableTag;
		public UInt32 checksum;
		public UInt32 offset;
		public UInt32 length;
	}
	public sealed class NameTable : FontTable<NameRecord> {
		readonly UInt16 version;
		readonly UInt16 count;
		readonly UInt16 storageOffset;
		readonly List<NameRecord> nameRecord = new List<NameRecord>();
		readonly UInt16 langTagCount;
		readonly List<LangTagRecord> langTagRecord = new List<LangTagRecord>();
		public NameTable(BinaryReader reader, UInt32 offset) : base(reader, offset) {
			version = reader.ReadUInt16();
			count = reader.ReadUInt16();
			storageOffset = reader.ReadUInt16();
			for (UInt16 i = 0; i < count; i++)
				nameRecord.Add(new NameRecord(
					reader.ReadUInt16(),
					reader.ReadUInt16(),
					reader.ReadUInt16(),
					(NameID)reader.ReadUInt16(),
					reader.ReadUInt16(),
					reader.ReadUInt16()
				));
			if (version == 1) {
				langTagCount = reader.ReadUInt16();
				for (UInt16 i = 0; i < langTagCount; i++)
					langTagRecord.Add(new LangTagRecord(
						reader.ReadUInt16(),
						reader.ReadUInt16()
					));
			}
			foreach (var i in nameRecord)
				i.Load(reader, offset + storageOffset);
			if (version == 1) {
				foreach (var i in langTagRecord)
					i.Load(reader, offset + storageOffset);
			}
		}
		public sealed override IReadOnlyList<NameRecord> GetItems() {
			return nameRecord;
		}
	}
	public class NameRecord {
		public UInt16 PlatformID { get; private set; }
		public UInt16 EncodingID { get; private set; }
		public UInt16 LanguageID { get; private set; }
		public NameID NameID { get; private set; }
		public UInt16 Length { get; private set; }
		public UInt16 StringOffset { get; private set; }
		public String Value { get; private set; }
		public NameRecord(UInt16 platformID, UInt16 encodingID, UInt16 languageID, NameID nameID, UInt16 length, UInt16 stringOffset) {
			PlatformID = platformID;
			EncodingID = encodingID;
			LanguageID = languageID;
			NameID = nameID;
			Length = length;
			StringOffset = stringOffset;
		}
		public void Load(BinaryReader reader, UInt32 origin) {
			reader.BaseStream.Position = origin + StringOffset;
			Encoding encoding;
			try {
				switch (PlatformID) {
					case 0: encoding = Encoding.BigEndianUnicode; break;
					case 1: encoding = Encoding.GetEncoding(10000 + EncodingID); break;
					case 3: encoding = Encoding.BigEndianUnicode; break;
					default: return;
				}
			}
			catch (NotSupportedException) { return; }
			catch (ArgumentException) { return; }
			Value = encoding.GetString(reader.ReadBytes(Length));
		}
	}
	public enum NameID : UInt16 {
		CopyrightNotice      =  0,
		FontFamilyName       =  1,
		FontSubfamilyName    =  2,
		UniqueFontIdentifier =  3,
		FullFontName         =  4,
		VersionString        =  5,
		PostScriptName       =  6,
		Trademark            =  7,
		ManufacturerName     =  8,
		Designer             =  9,
		Description          = 10,
		URLVendor            = 11,
		URLDesigner          = 12,
		LicenseDescription   = 13,
		LicenseInfoURL       = 14,

		TypographicFamilyName          = 16,
		TypographicSubfamilyName       = 17,
		CompatibleFull                 = 18,
		SampleText                     = 19,
		PostScriptCIDFindfontName      = 20,
		WWSFamilyName                  = 21,
		WWSSubfamilyName               = 22,
		LightBackgroundPalette         = 23,
		DarkBackgroundPalette          = 24,
		VariationsPostScriptNamePrefix = 25,
	}
	public class LangTagRecord {
		public UInt16 Length { get; private set; }
		public UInt16 LangTagOffset { get; private set; }
		public String Value { get; private set; }
		public LangTagRecord(UInt16 length, UInt16 langTagOffset) {
			Length = length;
			LangTagOffset = langTagOffset;
		}
		public void Load(BinaryReader reader, UInt32 origin) {
			reader.BaseStream.Position = origin + LangTagOffset;
			Value = Encoding.BigEndianUnicode.GetString(reader.ReadBytes(Length));
		}
	}
	public sealed class MetaTable : FontTable<DataMap> {
		readonly UInt32 version;
#pragma warning disable IDE0052 // Reserved
		readonly UInt32 flags;
#pragma warning restore IDE0052 // Reserved
		readonly UInt32 dataMapCount;
		readonly List<DataMap> dataMaps = new List<DataMap>();
		public MetaTable(BinaryReader reader, UInt32 offset) : base(reader, offset) {
			version = reader.ReadUInt32();
			if (version != 1) throw new NotSupportedException();
			flags = reader.ReadUInt32();
			reader.ReadUInt32();
			dataMapCount = reader.ReadUInt32();
			for (UInt32 i = 0; i < dataMapCount; i++)
				dataMaps.Add(new DataMap (
					reader.ReadTag(),
					reader.ReadUInt32(),
					reader.ReadUInt32()
				));
			foreach (var i in dataMaps)
				i.Load(reader, offset);
		}
		public sealed override IReadOnlyList<DataMap> GetItems() {
			return dataMaps;
		}
	}
	public class DataMap {
		public String Tag { get; private set; }
		public UInt32 DataOffset { get; private set; }
		public UInt32 DataLength { get; private set; }
		public String Value { get; private set; }
		public DataMap(String tag, UInt32 dataOffset, UInt32 dataLength) {
			Tag = tag;
			DataOffset = dataOffset;
			DataLength = dataLength;
		}
		public void Load(BinaryReader reader, UInt32 origin) {
			reader.BaseStream.Position = origin + DataOffset;
			Value = Encoding.ASCII.GetString(reader.ReadBytes((int)DataLength));
		}
	}
	public static class BinaryReaderExtensions {
		public static string ReadTag(this BinaryReader reader) {
			return Encoding.ASCII.GetString(reader.ReadBytes(4));
		}
	}
}
