//
// © 2017-2020 Qualcomm Technologies, Inc. and/or its subsidiaries. All rights reserved.
//

import UIKit
import CocoaLumberjack
import GCDWebServer

class HTTPServer {
	// MARK: Public Properties
	static let sharedInstance = HTTPServer()
	var localLibraryIdentifier: String?
	
	enum URLTypes {
		case artwork
		case audio
		case testAudioArtwork
		case testAudio
		
		fileprivate func toString() -> String {
			switch self {
			case .artwork: return "artwork"
			case .audio: return "audio"
			case .testAudioArtwork: return "testartwork"
			case .testAudio: return "test"
			}
		}
		
		fileprivate static func from(string: String) -> URLTypes? {
			if string == URLTypes.artwork.toString() {
				return .artwork
			}
			if string == URLTypes.audio.toString() {
				return .audio
			}
			if string == URLTypes.testAudioArtwork.toString() {
				return .testAudioArtwork
			}
			if string == URLTypes.testAudio.toString() {
				return .testAudio
			}
			return nil
		}
	}
	
	enum TestAudioFiles {
		case pinkNoiseWithSubwoofer
		case pinkNoiseWithoutSubwoofer
		
		fileprivate func data() throws -> Data {
			var path: String? = nil
			switch self {
			case .pinkNoiseWithoutSubwoofer:
				path = Bundle.main.path(forResource: "LR_LSRS_pinkNoise", ofType: "flac")
			case .pinkNoiseWithSubwoofer:
				path = Bundle.main.path(forResource: "LR_LFE_LSRS_pinkNoise", ofType: "flac")
			}
			if path == nil {
				throw CocoaError(.fileNoSuchFile)
			}
			let data = try Data(contentsOf: URL(fileURLWithPath: path!))
			return data
		}
		
		fileprivate func artworkData() throws -> Data {
			if let image = UIImage(named: "album-art-default-med"),
				let data = image.jpegData(compressionQuality: 0.9) {
				return data
			}
			
			throw CocoaError(.fileNoSuchFile)
		}
		
		fileprivate static func from(id: UInt64) -> TestAudioFiles? {
			if id == 1 {
				return .pinkNoiseWithoutSubwoofer
			} else if id == 2 {
				return .pinkNoiseWithSubwoofer
			}
			return nil
		}
		
		fileprivate func mimeType() -> String {
			return "audio/flac"
		}
		
		fileprivate func id() -> UInt64 {
			switch self {
			case .pinkNoiseWithoutSubwoofer:
				return 1
			case .pinkNoiseWithSubwoofer:
				return 2
			}
		}
	}
	
	// MARK: Private Properties/Types
	
	// Not an enum as need raw Int value
	private struct HTTPStatus {
		static let unavailable = GCDWebServerServerErrorHTTPStatusCode.httpStatusCode_ServiceUnavailable.rawValue
		static let badRequest = GCDWebServerClientErrorHTTPStatusCode.httpStatusCode_BadRequest.rawValue
		static let unsatisfiableRange = GCDWebServerClientErrorHTTPStatusCode.httpStatusCode_RequestedRangeNotSatisfiable.rawValue
		static let notFound = GCDWebServerClientErrorHTTPStatusCode.httpStatusCode_NotFound.rawValue
		static let ok = GCDWebServerSuccessfulHTTPStatusCode.httpStatusCode_OK.rawValue
		static let okPartial = GCDWebServerSuccessfulHTTPStatusCode.httpStatusCode_PartialContent.rawValue
	}
	
	private struct QueryKeys {
		static let library = "library"
		static let id = "id"
		static let type = "type"
	}
	
	private let server = GCDWebServer()
	private static let port:UInt = 55123
	
	// MARK: Initializer
	private init() {
		GCDWebServer.setLogLevel(0)
	}
}

// MARK: - Public Methods
extension HTTPServer {
	func start() {
		server.addDefaultHandler(forMethod: "GET",
								 request: GCDWebServerRequest.self,
								 asyncProcessBlock:{ [weak self] (request, completion) in
									guard let self = self else {
										completion(GCDWebServerResponse(statusCode: HTTPStatus.unavailable))
										return
									}
									
									self.handleRequest(request, completion: completion)
		})
		server.start(withPort: HTTPServer.port, bonjourName: "")
	}
	
	func urlForTestAudio(_ file: TestAudioFiles) -> String? {
		return urlForLocalAsset(id: file.id(), type: .testAudio)
	}
	
	func urlForTestArtwork(_ file: TestAudioFiles) -> String? {
		return urlForLocalAsset(id: file.id(), type: .testAudioArtwork)
	}
	
	func urlForLocalAsset(id: UInt64, type: URLTypes) -> String? {
		guard let serverAddress = server.serverURL,
			let libraryID = localLibraryIdentifier else {
				return nil
		}
		
		var components = URLComponents(url: serverAddress, resolvingAgainstBaseURL: false)
		
		components?.path = "/media"
		let libraryQuery = URLQueryItem(name: QueryKeys.library, value: libraryID)
		let idQuery = URLQueryItem(name: QueryKeys.id, value: String(id))
		let typeQuery = URLQueryItem(name: QueryKeys.type, value: type.toString())
		components?.queryItems = [libraryQuery, idQuery, typeQuery]
		
		return components?.url?.absoluteString
	}
	
	func refreshURLIfLocal(url: String) -> String {
		// Local URLs contain the IP of the device. This may/will change. To maintain currency of urls
		// this method extracts appropriate info from old URL and generates new URL for favorites etc.
		
		guard isLocalAssetURL(url) else {
			return url
		}
		
		guard let (id, type) = parseLocalAssetURL(url) else {
			return url
		}
		
		return urlForLocalAsset(id: id, type: type) ?? url
	}
	
	func isLocalAssetURL(_ url: String) -> Bool {
		guard let query = URLComponents(string: url)?.queryItems else {
			return false
		}
		
		guard let libraryQuery = query.filter({ $0.name == QueryKeys.library }).first,
			let libraryValue = libraryQuery.value else {
				return false
		}
		return libraryValue == localLibraryIdentifier
	}
	
	func parseLocalAssetURL(_ url: String) -> (id: UInt64, type: URLTypes)? {
		guard let query = URLComponents(string: url)?.queryItems else {
			return nil
		}
		
		guard let libraryQuery = query.first(where: { $0.name == QueryKeys.library }),
			let libraryValue = libraryQuery.value,
			libraryValue == localLibraryIdentifier else {
				return nil
		}
		
		guard let idQuery = query.first(where: { $0.name == QueryKeys.id }),
			let idValue = idQuery.value,
			let id = UInt64(idValue) else {
				return nil
		}
		
		guard let typeQuery = query.first(where: { $0.name == QueryKeys.type }),
			let typeValue = typeQuery.value,
			let type = URLTypes.from(string: typeValue) else {
				return nil
		}
		
		return (id, type)
	}
}

// MARK: - Private Methods
extension HTTPServer {
	private func handleRequest(_ request: GCDWebServerRequest, completion: @escaping GCDWebServerCompletionBlock) {
		let urlString = request.url.absoluteString
		DDLogVerbose("URL request for \(urlString)")
		guard isLocalAssetURL(urlString),
			let (id, type) = parseLocalAssetURL(urlString) else {
				completion(GCDWebServerResponse(statusCode: HTTPStatus.badRequest))
				return
		}
		
		switch type {
		case .artwork:
			handleImageRequest(id: id, completion: completion)
		case .audio:
			handleSongDataRequest(request, id: id, completion: completion)
		case .testAudio:
			handleTestAudioRequest(request, id: id, completion: completion)
		case .testAudioArtwork:
			handleTestArtworkRequest(id: id, completion: completion)
		}
	}
	
	private func handleImageRequest(id: UInt64, completion: @escaping GCDWebServerCompletionBlock) {
		DDLogVerbose("Image request for \(id)")
		guard let data = LocalMusicLibrary.sharedInstance.imageDataForSong(id: id,
																		   size: CGSize(width: 128.0, height: 128.0)) else {
																			completion(GCDWebServerResponse(statusCode: HTTPStatus.notFound))
																			return
		}
		let response = GCDWebServerDataResponse(data: data, contentType: "image/jpeg")
		response.statusCode = HTTPStatus.ok
		completion(response)
	}
	
	private func handleSongDataRequest(_ request: GCDWebServerRequest, id: UInt64, completion: @escaping GCDWebServerCompletionBlock) {
		LocalMusicLibrary.sharedInstance.dataForSong(id: id, completion: { dataOpt, mimeTypeOpt in
			if let data = dataOpt,
				let mimeType = mimeTypeOpt {
				self.respondWithData(data, mimeType: mimeType, request: request, completion: completion)
			} else {
				completion(GCDWebServerResponse(statusCode: HTTPStatus.notFound))
			}
		} )
	}
	
	private func respondWithData(_ data: Data,
								 mimeType: String,
								 request: GCDWebServerRequest,
								 completion: @escaping GCDWebServerCompletionBlock) {
		var response: GCDWebServerDataResponse?
		if request.hasByteRange() {
			var range = request.byteRange
			if range.location >= data.count ||
				range.location + range.length > data.count {
				// Unsatisfiable
				completion(GCDWebServerResponse(statusCode: HTTPStatus.unsatisfiableRange))
				return
			}
			
			if range.length < 0 {
				range.length = data.count - range.location
			}
			
			let subdata = data.subdata(in: range.location ..< range.location + range.length)
			response = GCDWebServerDataResponse(data: subdata, contentType: mimeType)
			response!.statusCode = HTTPStatus.okPartial
			response!.setValue("bytes \(range.location)-\(range.location+range.length-1)/\(range.location+range.length)",
				forAdditionalHeader: "Content-Range")
			response!.contentLength = UInt(range.length)
		} else {
			response = GCDWebServerDataResponse(data: data, contentType: mimeType)
			response!.statusCode = HTTPStatus.ok
			response!.contentLength = UInt(data.count)
		}
		response?.setValue("bytes", forAdditionalHeader: "Accept-Ranges")
		completion(response)
	}
	
	// MARK: Test audio/art
	private func handleTestAudioRequest(_ request: GCDWebServerRequest, id: UInt64, completion: @escaping GCDWebServerCompletionBlock) {
		if let testFile = TestAudioFiles.from(id: id) {
			do {
				let data = try testFile.data()
				respondWithData(data, mimeType: testFile.mimeType(), request: request, completion: completion)
			}
			catch let error {
				DDLogError("Unable to fetch test audio: \(error)")
				completion(GCDWebServerResponse(statusCode: HTTPStatus.notFound))
			}
		}
	}
	
	private func handleTestArtworkRequest(id: UInt64, completion: @escaping GCDWebServerCompletionBlock) {
		if let testFile = TestAudioFiles.from(id: id) {
			do {
				let data = try testFile.artworkData()
				let response = GCDWebServerDataResponse(data: data, contentType: "image/jpeg")
				response.statusCode = HTTPStatus.ok
				completion(response)
			}
			catch let error {
				DDLogError("Unable to fetch test artwork: \(error)")
				completion(GCDWebServerResponse(statusCode: HTTPStatus.notFound))
			}
		}
	}
}
