//
//  Generator.swift
//  IconGenerator
//
//  Created by 马强 on 2019/10/15.
//  Copyright © 2019 Arror. All rights reserved.
//

import AppKit
import Combine

public struct MessageError: LocalizedError {
        
    public let errorDescription: String?
    
    public init(_ message: String) {
        self.errorDescription = message
    }
}

public final class Generator: ObservableObject {
    
    private var cancellers: Set<AnyCancellable> = []
    
    @Published var image: NSImage?
    @Published var iPhoneAvailable: Bool = true
    @Published var iPadAvailable: Bool = false
    @Published var carAvailable: Bool = false
    @Published var macAvailable: Bool = false
    @Published var watchAvailable: Bool = false
    @Published var isGeneratorButtonEnable: Bool = false
        
    public init() {
        Publishers.CombineLatest(
            self.$image.map({ $0 != nil }),
            Publishers.CombineLatest(
                Publishers.CombineLatest(self.$iPhoneAvailable, self.$iPadAvailable).map({ $0 || $1 }),
                Publishers.CombineLatest3(self.$carAvailable, self.$macAvailable, self.$watchAvailable).map({ $0 || $1 || $2 })
            ).map({ $0 || $1  })
        ).map({ $0 && $1 }).assign(to: \Generator.isGeneratorButtonEnable, on: self).store(in: &self.cancellers)
    }
    
    private var icons: [Contents.Icon] {
        var reval: [Device] = []
        if self.iPhoneAvailable {
            reval.append(.iPhone)
        }
        if self.iPadAvailable {
            reval.append(.iPad)
        }
        if self.carAvailable {
            reval.append(.car)
        }
        if self.macAvailable {
            reval.append(.mac)
        }
        if self.watchAvailable {
            reval.append(.watch)
        }
        return reval.allIcons
    }
    
    public func export(at url: URL) throws {
        guard let image = self.image else {
            return
        }
        do {
            let directoryURL = url.appendingPathComponent("AppIcon.appiconset", isDirectory: true)
            if FileManager.default.fileExists(atPath: directoryURL.path) {
                try FileManager.default.removeItem(at: directoryURL)
            }
            try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
            let icons = self.icons
            var set: Set<String> = []
            for icon in icons {
                if !set.contains(icon.fileName) {
                    try image.write(to: directoryURL.appendingPathComponent(icon.fileName), size: icon.size)
                }
                set.insert(icon.fileName)
            }
            let contentsJSON = try JSONSerialization.jsonObject(with: try JSONEncoder().encode(Contents(images: icons)), options: [])
            let contentsData = try JSONSerialization.data(withJSONObject: contentsJSON, options: .prettyPrinted)
            try contentsData.write(to: directoryURL.appendingPathComponent("Contents.json"), options: .atomicWrite)
            
        } catch {
            throw MessageError("导出失败")
        }
    }
}

extension NSImage {
    
    public static func fetchImage(at url: URL?) throws -> NSImage {
        guard let referenceURL = url else {
            throw MessageError("读取图片路径失败")
        }
        let filePath: String
        do {
            filePath = try String(contentsOf: referenceURL)
        } catch {
            throw MessageError("读取图片路径失败")
        }
        guard let rawPath = filePath.removingPercentEncoding else {
            throw MessageError("读取图片路径失败")
        }
        let fileURL = URL(fileURLWithPath: rawPath).standardized
        guard fileURL.pathExtension.lowercased() == "png" else {
            throw MessageError("图片格式必须为PNG格式")
        }
        let imageData: Data
        do {
            imageData = try Data(contentsOf: fileURL)
        } catch {
            throw MessageError("读取图片失败")
        }
        guard let image = NSImage(data: imageData) else {
            throw MessageError("读取图片失败")
        }
        guard image.size == NSSize(width: 1024, height: 1024) else {
            throw MessageError("图片尺寸必须为1024 * 1024")
        }
        return image
    }
    
    private func resize(to size: NSSize) throws -> NSImage {
        guard let rep = self.bestRepresentation(for: NSRect(origin: .zero, size: size), context: nil, hints: nil) else {
            throw MessageError("图片压缩失败")
        }
        return NSImage(size: size, flipped: false) { _ in
            return rep.draw(in: NSRect(origin: .zero, size: size))
        }
    }
    
    private func fetchResizeImageData(size: NSSize) throws -> Data {
        let reval = try self.resize(to: size)
        guard let imageData = reval.tiffRepresentation else {
            throw MessageError("生成图片数据失败")
        }
        
        guard let rep = NSBitmapImageRep(data: imageData) else {
            throw MessageError("生成位图失败")
        }
        guard let data = rep.representation(using: .png, properties: [:]) else {
            throw MessageError("生成图片数据失败")
        }
        return data
    }
    
    public func write(to url: URL, size: NSSize) throws {
        do {
            try self.fetchResizeImageData(size: size).write(to: url, options: .atomicWrite)
        } catch {
            throw MessageError("写入图片失败")
        }
    }
}

public enum Device: String {
    
    case iPhone
    case iPad
    case car
    case watch
    case mac
    
    var name: String {
        switch self {
        case .iPhone:
            return "iPhone"
        case .iPad:
            return "iPad"
        case .car:
            return "CarPlay"
        case .mac:
            return "Mac"
        case .watch:
            return "Apple Watch"
        }
    }
}

public struct Contents: Encodable {
    
    public struct Info: Encodable {
        
        public let version: Int
        public let author: String
        
        public init(version: Int, author: String) {
            self.version = version
            self.author = author
        }
    }
    
    public struct Icon: Encodable {
        
        private static let format: NumberFormatter = {
            let reval = NumberFormatter()
            reval.maximumFractionDigits = 1
            return reval
        }()
        
        public enum Idiom: String {
            case iPhone                 = "iphone"
            case iPad                   = "ipad"
            case iOSMarketing           = "ios-marketing"
            case appleWatch             = "watch"
            case appleWatchMarketing    = "watch-marketing"
            case carplay                = "car"
            case mac                    = "mac"
        }
        
        private let length: CGFloat
        private let idiom: Idiom
        private let scale: Int
        private let role: String?
        private let subtype: String?
        private let sizeFormatString: String
        
        public var fileName: String {
            return "icon_\(Int(self.size.width))x\(Int(self.size.height)).png"
        }
        
        public var size: NSSize {
            let l = CGFloat(self.scale) * self.length
            return NSSize(width: l, height: l)
        }
        
        public init(length: CGFloat, idiom: Idiom, scale: Int, role: String?, subtype: String?) {
            let lengthString = Icon.format.string(from: NSNumber(value: Double(length))) ?? ""
            self.sizeFormatString = "\(lengthString)x\(lengthString)"
            self.length = length
            self.idiom = idiom
            self.scale = scale
            self.role = role
            self.subtype = subtype
        }
        
        private enum CodingKeys: String, CodingKey {
            case size = "size"
            case idiom = "idiom"
            case scale = "scale"
            case role = "role"
            case subtype = "subtype"
            case fileName = "filename"
        }
        
        public func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(self.sizeFormatString, forKey: .size)
            try container.encode(self.idiom.rawValue, forKey: .idiom)
            try container.encode(self.fileName, forKey: .fileName)
            try container.encode("\(self.scale)x", forKey: .scale)
            if let reval = self.role {
                try container.encode(reval, forKey: .role)
            }
            if let reval = self.subtype {
                try container.encode(reval, forKey: .subtype)
            }
        }
    }
        
    public let info = Info(version: 1, author: "xcode")
    public let images: [Icon]
    
    public init(images: [Icon]) {
        self.images = images
    }
}

extension Device {
    
    static var iOSMarketingIcon: Contents.Icon {
        return Contents.Icon(length: 1024.0, idiom: .iOSMarketing, scale: 1, role: nil, subtype: nil)
    }
    
    static var watchMarketingIcon: Contents.Icon {
        return Contents.Icon(length: 1024.0, idiom: .appleWatchMarketing, scale: 1, role: nil, subtype: nil)
    }
}

extension Device {
        
    var infos: [Contents.Icon] {
        switch self {
        case .iPhone:
            return [
                Contents.Icon(length: 20.0, idiom: .iPhone, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 20.0, idiom: .iPhone, scale: 3, role: nil, subtype: nil),
                Contents.Icon(length: 29.0, idiom: .iPhone, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 29.0, idiom: .iPhone, scale: 3, role: nil, subtype: nil),
                Contents.Icon(length: 40.0, idiom: .iPhone, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 40.0, idiom: .iPhone, scale: 3, role: nil, subtype: nil),
                Contents.Icon(length: 60.0, idiom: .iPhone, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 60.0, idiom: .iPhone, scale: 3, role: nil, subtype: nil)
            ]
        case .iPad:
            return [
                Contents.Icon(length: 20.0, idiom: .iPad, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 20.0, idiom: .iPad, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 29.0, idiom: .iPad, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 29.0, idiom: .iPad, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 40.0, idiom: .iPad, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 40.0, idiom: .iPad, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 76.0, idiom: .iPad, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 76.0, idiom: .iPad, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 83.5, idiom: .iPad, scale: 2, role: nil, subtype: nil)
            ]
        case .car:
            return [
                Contents.Icon(length: 60.0, idiom: .carplay, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 60.0, idiom: .carplay, scale: 3, role: nil, subtype: nil)
            ]
        case .mac:
            return [
                Contents.Icon(length: 16.0, idiom: .mac, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 16.0, idiom: .mac, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 32.0, idiom: .mac, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 32.0, idiom: .mac, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 128.0, idiom: .mac, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 128.0, idiom: .mac, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 256.0, idiom: .mac, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 256.0, idiom: .mac, scale: 2, role: nil, subtype: nil),
                Contents.Icon(length: 512.0, idiom: .mac, scale: 1, role: nil, subtype: nil),
                Contents.Icon(length: 512.0, idiom: .mac, scale: 2, role: nil, subtype: nil)
            ]
        case .watch:
            return [
                Contents.Icon(length: 24.0, idiom: .appleWatch, scale: 2, role: "notificationCenter", subtype: "38mm"),
                Contents.Icon(length: 27.5, idiom: .appleWatch, scale: 2, role: "notificationCenter", subtype: "42mm"),
                Contents.Icon(length: 29.0, idiom: .appleWatch, scale: 2, role: "companionSettings", subtype: nil),
                Contents.Icon(length: 29.0, idiom: .appleWatch, scale: 3, role: "companionSettings", subtype: nil),
                Contents.Icon(length: 40.0, idiom: .appleWatch, scale: 2, role: "appLauncher", subtype: "38mm"),
                Contents.Icon(length: 44.0, idiom: .appleWatch, scale: 2, role: "appLauncher", subtype: "40mm"),
                Contents.Icon(length: 50.0, idiom: .appleWatch, scale: 2, role: "appLauncher", subtype: "44mm"),
                Contents.Icon(length: 86.0, idiom: .appleWatch, scale: 2, role: "quickLook", subtype: "38mm"),
                Contents.Icon(length: 98.0, idiom: .appleWatch, scale: 2, role: "quickLook", subtype: "42mm"),
                Contents.Icon(length: 108.0, idiom: .appleWatch, scale: 2, role: "quickLook", subtype: "44mm")
            ]
        }
    }
}

extension Array where Element == Device {
    
    var allIcons: [Contents.Icon] {
        
        let set = Set(self)
        
        var reval: [Contents.Icon] = []
        if set.contains(.iPhone) {
            reval.append(contentsOf: Device.iPhone.infos)
        }
        if set.contains(.iPad) {
            reval.append(contentsOf: Device.iPad.infos)
        }
        if set.contains(.iPhone) || set.contains(.iPad) {
            reval.append(Device.iOSMarketingIcon)
        }
        if set.contains(.car) {
            reval.append(contentsOf: Device.car.infos)
        }
        if set.contains(.mac) {
            reval.append(contentsOf: Device.mac.infos)
        }
        if set.contains(.watch) {
            reval.append(contentsOf: Device.watch.infos)
            reval.append(Device.watchMarketingIcon)
        }
        
        return reval
    }
}
