//
//  UTI.swift
//  fseventstool
//
//  Created by Matthias Keiser on 09.01.17.
//  Copyright © 2017 Tristan Inc. All rights reserved.
//

import Foundation

#if os(iOS) || os(watchOS)
	import MobileCoreServices
#elseif os(macOS)
	import CoreServices
#endif

/// Instances of the UTI class represent a specific Universal Type Identifier, e.g. kUTTypeMPEG4.

public class UTI: RawRepresentable, Equatable {

	/**
	The TagClass enum represents the supported tag classes.
	
	- fileExtension: kUTTagClassFilenameExtension
	- mimeType: kUTTagClassMIMEType
	- pbType: kUTTagClassNSPboardType
	- osType: kUTTagClassOSType
	*/
	public enum TagClass: String {

		/// Equivalent to kUTTagClassFilenameExtension
		case fileExtension = "public.filename-extension"

		/// Equivalent to kUTTagClassMIMEType
		case mimeType = "public.mime-type"

		#if os (macOS)

		/// Equivalent to kUTTagClassNSPboardType
		case pbType =  "com.apple.nspboard-type"

		/// Equivalent to kUTTagClassOSType
		case osType =  "com.apple.ostype"
		#endif

		/// Convenience variable for internal use.
		
		fileprivate var rawCFValue: CFString {
			return self.rawValue as CFString
		}
	}

	public typealias RawValue = String
	public let rawValue: String


	/// Convenience variable for internal use.

	private var rawCFValue: CFString {

		return self.rawValue as CFString
	}

	// MARK: Initialization


	/**
	
	This is the designated initializer of the UTI class.
	
	 - Parameters:
			- rawValue: A string that is a Universal Type Identifier, i.e. "com.foobar.baz" or a constant like kUTTypeMP3.
	 - Returns:
			An UTI instance representing the specified rawValue.
	 - Note:
			You should rarely use this method. The preferred way to initialize a known UTI is to use its static variable (i.e. UTI.pdf). You should make an extension to make your own types available as static variables.
	
	*/

	public required init(rawValue: UTI.RawValue) {

		self.rawValue = rawValue
	}

	/**

	Initialize an UTI with a tag of a specified class.

	- Parameters:
		- tagClass: The class of the tag.
		- value: The value of the tag.
		- conformingTo: If specified, the returned UTI must conform to this UTI. If nil is specified, this parameter is ignored. The default is nil.
	- Returns:
		An UTI instance representing the specified rawValue. If no known UTI with the specified tags is found, a dynamic UTI is created.
	- Note:
		You should rarely need this method. It's usually simpler to use one of the specialized initialzers like
		```convenience init?(withExtension fileExtension: String, conformingTo conforming: UTI? = nil)```
	*/

	public convenience init(withTagClass tagClass: TagClass, value: String, conformingTo conforming: UTI? = nil) {

		let unmanagedIdentifier = UTTypeCreatePreferredIdentifierForTag(tagClass.rawCFValue, value as CFString, conforming?.rawCFValue)

		// UTTypeCreatePreferredIdentifierForTag only returns nil if the tag class is unknwown, which can't happen to us since we use an
		// enum of known values. Hence we can force-cast the result.

        // swiftlint:disable force_unwrapping
        let identifier = (unmanagedIdentifier?.takeRetainedValue() as String?)!
        // swiftlint:enable force_unwrapping

		self.init(rawValue: identifier)
	}

	/**

	Initialize an UTI with a file extension.
	
	- Parameters:
		- withExtension: The file extension (e.g. "txt").
		- conformingTo: If specified, the returned UTI must conform to this UTI. If nil is specified, this parameter is ignored. The default is nil.
	- Returns: 
		An UTI corresponding to the specified values.
	**/

	public convenience init(withExtension fileExtension: String, conformingTo conforming: UTI? = nil) {

		self.init(withTagClass: .fileExtension, value: fileExtension, conformingTo: conforming)
	}

	/**

	Initialize an UTI with a MIME type.
	
	- Parameters:
		- mimeType: The MIME type (e.g. "text/plain").
		- conformingTo: If specified, the returned UTI must conform to this UTI. If nil is specified, this parameter is ignored. The default is nil.
	- Returns:
		An UTI corresponding to the specified values.
	*/

	public convenience init(withMimeType mimeType: String, conformingTo conforming: UTI? = nil) {

		self.init(withTagClass: .mimeType, value: mimeType, conformingTo: conforming)
	}

	#if os(macOS)

	/**

	Initialize an UTI with a pasteboard type.
    - Important: **This function is de-facto deprecated!** The old cocoa pasteboard types ( `NSStringPboardType`, `NSPDFPboardType`, etc) have been deprecated in favour of actual UTIs, and the
     constants are not available anymore in Swift. This function only works correctly with the values of these old constants, but _not_ with the replacement values (like `NSPasteboardTypeString` etc), since these
     already are UTIs.
	- Parameters:
		- pbType: The pasteboard type (e.g. NSPDFPboardType).
		- conformingTo: If specified, the returned UTI must conform to this UTI. If nil is specified, this parameter is ignored. The default is nil.
	- Returns:
		An UTI corresponding to the specified values.
	*/
	public convenience init(withPBType pbType: String, conformingTo conforming: UTI? = nil) {

		self.init(withTagClass: .pbType, value: pbType, conformingTo: conforming)
	}

	/**
	Initialize an UTI with a OSType.
	
	- Parameters:
		- osType: The OSType type as a string (e.g. "PDF ").
		- conformingTo: If specified, the returned UTI must conform to this UTI. If nil is specified, this parameter is ignored. The default is nil.
	- Returns:
		An UTI corresponding to the specified values.
	- Note:
		You can use the variable ```OSType.string``` to get a string from an actual OSType.
	*/

	public convenience init(withOSType osType: String, conformingTo conforming: UTI? = nil) {

		self.init(withTagClass: .osType, value: osType, conformingTo: conforming)
	}

	#endif

	// MARK: Accessing Tags

	/**

	Returns the tag with the specified class.
	
	- Parameters:
		- tagClass: The tag class to return.
	- Returns:
		The requested tag, or nil if there is no tag of the specified class.
	*/

	public func tag(with tagClass: TagClass) -> String? {

		let unmanagedTag = UTTypeCopyPreferredTagWithClass(self.rawCFValue, tagClass.rawCFValue)

		guard let tag = unmanagedTag?.takeRetainedValue() as String? else {
			return nil
		}

		return tag
	}

	/// Return the file extension that corresponds the the UTI. Returns nil if not available.

	public var fileExtension: String? {

		return self.tag(with: .fileExtension)
	}

	/// Return the MIME type that corresponds the the UTI. Returns nil if not available.

	public var mimeType: String? {

		return self.tag(with: .mimeType)
	}

	#if os(macOS)

	/// Return the pasteboard type that corresponds the the UTI. Returns nil if not available.

	public var pbType: String? {

		return self.tag(with: .pbType)
	}

	/// Return the OSType as a string that corresponds the the UTI. Returns nil if not available.
	/// - Note: you can use the ```init(with string: String)``` initializer to construct an actual OSType from the returnes string.

	public var osType: String? {

		return self.tag(with: .osType)
	}

	#endif

	/**

	Returns all tags of the specified tag class.
	
	- Parameters:
		- tagClass: The class of the requested tags.
	- Returns:
		An array of all tags of the receiver of the specified class.
	*/

	public func tags(with tagClass: TagClass) -> Array<String> {

		let unmanagedTags = UTTypeCopyAllTagsWithClass(self.rawCFValue, tagClass.rawCFValue)

		guard let tags = unmanagedTags?.takeRetainedValue() as? Array<CFString> else {
			return []
		}

		return tags as Array<String>
	}

	// MARK: List all UTIs associated with a tag


	/**
	Returns all UTIs that are associated with a specified tag.
	
	- Parameters:
	  - tag: The class of the specified tag.
	  - value: The value of the tag.
	  - conforming: If specified, the returned UTIs must conform to this UTI. If nil is specified, this parameter is ignored. The default is nil.
	- Returns:
		An array of all UTIs that satisfy the specified parameters.
	*/

	public static func utis(for tag: TagClass, value: String, conformingTo conforming: UTI? = nil) -> Array<UTI> {

		let unmanagedIdentifiers = UTTypeCreateAllIdentifiersForTag(tag.rawCFValue, value as CFString, conforming?.rawCFValue)


		guard let identifiers = unmanagedIdentifiers?.takeRetainedValue() as? Array<CFString> else {
			return []
		}

		return identifiers.compactMap { UTI(rawValue: $0 as String) }
	}

	// MARK: Equality and Conformance to other UTIs

	/**

	Checks if the receiver conforms to a specified UTI.
	
	- Parameters:
		- otherUTI: The UTI to which the receiver is compared.
	- Returns:
		```true``` if the receiver conforms to the specified UTI, ```false```otherwise.
	*/

	public func conforms(to otherUTI: UTI) -> Bool {

		return UTTypeConformsTo(self.rawCFValue, otherUTI.rawCFValue) as Bool
	}

	public static func == (lhs: UTI, rhs: UTI) -> Bool {

		return UTTypeEqual(lhs.rawCFValue, rhs.rawCFValue) as Bool
	}

	// MARK: Accessing Information about an UTI

	/// Returns the localized, user-readable type description string associated with a uniform type identifier.
	
	public var description: String? {

		let unmanagedDescription = UTTypeCopyDescription(self.rawCFValue)

		guard let description = unmanagedDescription?.takeRetainedValue() as String? else {
			return nil
		}

		return description
	}

	/// Returns a uniform type’s declaration as a Dictionary, or nil if if no declaration for that type can be found.

	public var declaration: [AnyHashable: Any]? {

		let unmanagedDeclaration = UTTypeCopyDeclaration(self.rawCFValue)

		guard let declaration = unmanagedDeclaration?.takeRetainedValue() as? [AnyHashable: Any] else {
			return nil
		}

		return declaration
	}

	/// Returns the location of a bundle containing the declaration for a type, or nil if the bundle could not be located.

	public var declaringBundleURL: URL? {

		let unmanagedURL = UTTypeCopyDeclaringBundleURL(self.rawCFValue)

		guard let url = unmanagedURL?.takeRetainedValue() as URL? else {
			return nil
		}

		return url
	}

	/// Returns ```true``` if the receiver is a dynamic UTI.

	public var isDynamic: Bool {

		return UTTypeIsDynamic(self.rawCFValue)
	}
}


// MARK: System defined UTIs

public extension UTI {

	static       let  item                        =    UTI(rawValue: kUTTypeItem                        as  String)
	static       let  content                     =    UTI(rawValue: kUTTypeContent                     as  String)
	static       let  compositeContent            =    UTI(rawValue: kUTTypeCompositeContent            as  String)
	static       let  message                     =    UTI(rawValue: kUTTypeMessage                     as  String)
	static       let  contact                     =    UTI(rawValue: kUTTypeContact                     as  String)
	static       let  archive                     =    UTI(rawValue: kUTTypeArchive                     as  String)
	static       let  diskImage                   =    UTI(rawValue: kUTTypeDiskImage                   as  String)
	static       let  data                        =    UTI(rawValue: kUTTypeData                        as  String)
	static       let  directory                   =    UTI(rawValue: kUTTypeDirectory                   as  String)
	static       let  resolvable                  =    UTI(rawValue: kUTTypeResolvable                  as  String)
	static       let  symLink                     =    UTI(rawValue: kUTTypeSymLink                     as  String)
	static       let  executable                  =    UTI(rawValue: kUTTypeExecutable                  as  String)
	static       let  mountPoint                  =    UTI(rawValue: kUTTypeMountPoint                  as  String)
	static       let  aliasFile                   =    UTI(rawValue: kUTTypeAliasFile                   as  String)
	static       let  aliasRecord                 =    UTI(rawValue: kUTTypeAliasRecord                 as  String)
	static       let  urlBookmarkData             =    UTI(rawValue: kUTTypeURLBookmarkData             as  String)
	static       let  url                         =    UTI(rawValue: kUTTypeURL                         as  String)
	static       let  fileURL                     =    UTI(rawValue: kUTTypeFileURL                     as  String)
	static       let  text                        =    UTI(rawValue: kUTTypeText                        as  String)
	static       let  plainText                   =    UTI(rawValue: kUTTypePlainText                   as  String)
	static       let  utf8PlainText               =    UTI(rawValue: kUTTypeUTF8PlainText               as  String)
	static       let  utf16ExternalPlainText      =    UTI(rawValue: kUTTypeUTF16ExternalPlainText      as  String)
	static       let  utf16PlainText              =    UTI(rawValue: kUTTypeUTF16PlainText              as  String)
	static       let  delimitedText               =    UTI(rawValue: kUTTypeDelimitedText               as  String)
	static       let  commaSeparatedText          =    UTI(rawValue: kUTTypeCommaSeparatedText          as  String)
	static       let  tabSeparatedText            =    UTI(rawValue: kUTTypeTabSeparatedText            as  String)
	static       let  utf8TabSeparatedText        =    UTI(rawValue: kUTTypeUTF8TabSeparatedText        as  String)
	static       let  rtf                         =    UTI(rawValue: kUTTypeRTF                         as  String)
	static       let  html                        =    UTI(rawValue: kUTTypeHTML                        as  String)
	static       let  xml                         =    UTI(rawValue: kUTTypeXML                         as  String)
	static       let  sourceCode                  =    UTI(rawValue: kUTTypeSourceCode                  as  String)
	static       let  assemblyLanguageSource      =    UTI(rawValue: kUTTypeAssemblyLanguageSource      as  String)
	static       let  cSource                     =    UTI(rawValue: kUTTypeCSource                     as  String)
	static       let  objectiveCSource            =    UTI(rawValue: kUTTypeObjectiveCSource            as  String)
	@available( OSX 10.11, iOS 9.0, * )
	static       let  swiftSource				  =    UTI(rawValue: kUTTypeSwiftSource				 as  String)
	static       let  cPlusPlusSource             =    UTI(rawValue: kUTTypeCPlusPlusSource             as  String)
	static       let  objectiveCPlusPlusSource    =    UTI(rawValue: kUTTypeObjectiveCPlusPlusSource    as  String)
	static       let  cHeader                     =    UTI(rawValue: kUTTypeCHeader                     as  String)
	static       let  cPlusPlusHeader             =    UTI(rawValue: kUTTypeCPlusPlusHeader             as  String)
	static       let  javaSource                  =    UTI(rawValue: kUTTypeJavaSource                  as  String)
	static       let  script                      =    UTI(rawValue: kUTTypeScript                      as  String)
	static       let  appleScript                 =    UTI(rawValue: kUTTypeAppleScript                 as  String)
	static       let  osaScript                   =    UTI(rawValue: kUTTypeOSAScript                   as  String)
	static       let  osaScriptBundle             =    UTI(rawValue: kUTTypeOSAScriptBundle             as  String)
	static       let  javaScript                  =    UTI(rawValue: kUTTypeJavaScript                  as  String)
	static       let  shellScript                 =    UTI(rawValue: kUTTypeShellScript                 as  String)
	static       let  perlScript                  =    UTI(rawValue: kUTTypePerlScript                  as  String)
	static       let  pythonScript                =    UTI(rawValue: kUTTypePythonScript                as  String)
	static       let  rubyScript                  =    UTI(rawValue: kUTTypeRubyScript                  as  String)
	static       let  phpScript                   =    UTI(rawValue: kUTTypePHPScript                   as  String)
	static       let  json                        =    UTI(rawValue: kUTTypeJSON                        as  String)
	static       let  propertyList                =    UTI(rawValue: kUTTypePropertyList                as  String)
	static       let  xmlPropertyList             =    UTI(rawValue: kUTTypeXMLPropertyList             as  String)
	static       let  binaryPropertyList          =    UTI(rawValue: kUTTypeBinaryPropertyList          as  String)
	static       let  pdf                         =    UTI(rawValue: kUTTypePDF                         as  String)
	static       let  rtfd                        =    UTI(rawValue: kUTTypeRTFD                        as  String)
	static       let  flatRTFD                    =    UTI(rawValue: kUTTypeFlatRTFD                    as  String)
	static       let  txnTextAndMultimediaData    =    UTI(rawValue: kUTTypeTXNTextAndMultimediaData    as  String)
	static       let  webArchive                  =    UTI(rawValue: kUTTypeWebArchive                  as  String)
	static       let  image                       =    UTI(rawValue: kUTTypeImage                       as  String)
	static       let  jpeg                        =    UTI(rawValue: kUTTypeJPEG                        as  String)
	static       let  jpeg2000                    =    UTI(rawValue: kUTTypeJPEG2000                    as  String)
	static       let  tiff                        =    UTI(rawValue: kUTTypeTIFF                        as  String)
	static       let  pict                        =    UTI(rawValue: kUTTypePICT                        as  String)
	static       let  gif                         =    UTI(rawValue: kUTTypeGIF                         as  String)
	static       let  png                         =    UTI(rawValue: kUTTypePNG                         as  String)
	static       let  quickTimeImage              =    UTI(rawValue: kUTTypeQuickTimeImage              as  String)
	static       let  appleICNS                   =    UTI(rawValue: kUTTypeAppleICNS                   as  String)
	static       let  bmp                         =    UTI(rawValue: kUTTypeBMP                         as  String)
	static       let  ico                         =    UTI(rawValue: kUTTypeICO                         as  String)
	static       let  rawImage                    =    UTI(rawValue: kUTTypeRawImage                    as  String)
	static       let  scalableVectorGraphics      =    UTI(rawValue: kUTTypeScalableVectorGraphics      as  String)
	@available(OSX 10.12, iOS 9.1, watchOS 2.1, *)
	static       let  livePhoto					  =    UTI(rawValue: kUTTypeLivePhoto					 as  String)
	@available(OSX 10.12, iOS 9.1, *)
	static       let  audiovisualContent          =    UTI(rawValue: kUTTypeAudiovisualContent          as  String)
	static       let  movie                       =    UTI(rawValue: kUTTypeMovie                       as  String)
	static       let  video                       =    UTI(rawValue: kUTTypeVideo                       as  String)
	static       let  audio                       =    UTI(rawValue: kUTTypeAudio                       as  String)
	static       let  quickTimeMovie              =    UTI(rawValue: kUTTypeQuickTimeMovie              as  String)
	static       let  mpeg                        =    UTI(rawValue: kUTTypeMPEG                        as  String)
	static       let  mpeg2Video                  =    UTI(rawValue: kUTTypeMPEG2Video                  as  String)
	static       let  mpeg2TransportStream        =    UTI(rawValue: kUTTypeMPEG2TransportStream        as  String)
	static       let  mp3                         =    UTI(rawValue: kUTTypeMP3                         as  String)
	static       let  mpeg4                       =    UTI(rawValue: kUTTypeMPEG4                       as  String)
	static       let  mpeg4Audio                  =    UTI(rawValue: kUTTypeMPEG4Audio                  as  String)
	static       let  appleProtectedMPEG4Audio    =    UTI(rawValue: kUTTypeAppleProtectedMPEG4Audio    as  String)
	static       let  appleProtectedMPEG4Video    =    UTI(rawValue: kUTTypeAppleProtectedMPEG4Video    as  String)
	static       let  aviMovie                    =    UTI(rawValue: kUTTypeAVIMovie                    as  String)
	static       let  audioInterchangeFileFormat  =    UTI(rawValue: kUTTypeAudioInterchangeFileFormat  as  String)
	static       let  waveformAudio               =    UTI(rawValue: kUTTypeWaveformAudio               as  String)
	static       let  midiAudio                   =    UTI(rawValue: kUTTypeMIDIAudio                   as  String)
	static       let  playlist                    =    UTI(rawValue: kUTTypePlaylist                    as  String)
	static       let  m3UPlaylist                 =    UTI(rawValue: kUTTypeM3UPlaylist                 as  String)
	static       let  folder                      =    UTI(rawValue: kUTTypeFolder                      as  String)
	static       let  volume                      =    UTI(rawValue: kUTTypeVolume                      as  String)
	static       let  package                     =    UTI(rawValue: kUTTypePackage                     as  String)
	static       let  bundle                      =    UTI(rawValue: kUTTypeBundle                      as  String)
	static       let  pluginBundle                =    UTI(rawValue: kUTTypePluginBundle                as  String)
	static       let  spotlightImporter           =    UTI(rawValue: kUTTypeSpotlightImporter           as  String)
	static       let  quickLookGenerator          =    UTI(rawValue: kUTTypeQuickLookGenerator          as  String)
	static       let  xpcService                  =    UTI(rawValue: kUTTypeXPCService                  as  String)
	static       let  framework                   =    UTI(rawValue: kUTTypeFramework                   as  String)
	static       let  application                 =    UTI(rawValue: kUTTypeApplication                 as  String)
	static       let  applicationBundle           =    UTI(rawValue: kUTTypeApplicationBundle           as  String)
	static       let  applicationFile             =    UTI(rawValue: kUTTypeApplicationFile             as  String)
	static       let  unixExecutable              =    UTI(rawValue: kUTTypeUnixExecutable              as  String)
	static       let  windowsExecutable           =    UTI(rawValue: kUTTypeWindowsExecutable           as  String)
	static       let  javaClass                   =    UTI(rawValue: kUTTypeJavaClass                   as  String)
	static       let  javaArchive                 =    UTI(rawValue: kUTTypeJavaArchive                 as  String)
	static       let  systemPreferencesPane       =    UTI(rawValue: kUTTypeSystemPreferencesPane       as  String)
	static       let  gnuZipArchive               =    UTI(rawValue: kUTTypeGNUZipArchive               as  String)
	static       let  bzip2Archive                =    UTI(rawValue: kUTTypeBzip2Archive                as  String)
	static       let  zipArchive                  =    UTI(rawValue: kUTTypeZipArchive                  as  String)
	static       let  spreadsheet                 =    UTI(rawValue: kUTTypeSpreadsheet                 as  String)
	static       let  presentation                =    UTI(rawValue: kUTTypePresentation                as  String)
	static       let  database                    =    UTI(rawValue: kUTTypeDatabase                    as  String)
	static       let  vCard                       =    UTI(rawValue: kUTTypeVCard                       as  String)
	static       let  toDoItem                    =    UTI(rawValue: kUTTypeToDoItem                    as  String)
	static       let  calendarEvent               =    UTI(rawValue: kUTTypeCalendarEvent               as  String)
	static       let  emailMessage                =    UTI(rawValue: kUTTypeEmailMessage                as  String)
	static       let  internetLocation            =    UTI(rawValue: kUTTypeInternetLocation            as  String)
	static       let  inkText                     =    UTI(rawValue: kUTTypeInkText                     as  String)
	static       let  font                        =    UTI(rawValue: kUTTypeFont                        as  String)
	static       let  bookmark                    =    UTI(rawValue: kUTTypeBookmark                    as  String)
	static       let  _3DContent                  =    UTI(rawValue: kUTType3DContent                   as  String)
	static       let  pkcs12                      =    UTI(rawValue: kUTTypePKCS12                      as  String)
	static       let  x509Certificate             =    UTI(rawValue: kUTTypeX509Certificate             as  String)
	static       let  electronicPublication       =    UTI(rawValue: kUTTypeElectronicPublication       as  String)
	static       let  log                         =    UTI(rawValue: kUTTypeLog                         as  String)
}

#if os(OSX)

	extension OSType {


		/// Returns the OSType encoded as a String.

		var string: String {

			let unmanagedString = UTCreateStringForOSType(self)

			return unmanagedString.takeRetainedValue() as String
		}


		/// Initializes a OSType from a String.
		///
		/// - Parameter string: A String representing an OSType.
		
		init(with string: String) {
			
			self = UTGetOSTypeFromString(string as CFString)
		}
	}
	
#endif
